├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── client ├── app │ ├── api │ │ └── api.js │ ├── components │ │ ├── App.jsx │ │ ├── AppNotification.jsx │ │ ├── Auth.jsx │ │ ├── Checkbox.jsx │ │ ├── ContentEditable.jsx │ │ ├── Header.jsx │ │ ├── IconButton.jsx │ │ ├── Image.jsx │ │ ├── Note.jsx │ │ ├── NoteBoard.jsx │ │ ├── Notification.jsx │ │ ├── Sharer.jsx │ │ ├── Start.jsx │ │ └── UsernameChecker.jsx │ ├── img │ │ ├── confirm.png │ │ ├── continue.png │ │ ├── delete.png │ │ ├── edit.png │ │ ├── git-logo.png │ │ ├── icon.png │ │ ├── loading.svg │ │ ├── new.png │ │ ├── no.png │ │ ├── notfound.png │ │ ├── retry.png │ │ └── share.png │ ├── index.jsx │ ├── models │ │ ├── Note.js │ │ └── Notification.js │ ├── stores │ │ ├── NoteBoardStore.js │ │ └── UiStore.js │ ├── style │ │ ├── checkbox.css │ │ ├── global.css │ │ ├── header.css │ │ ├── note.css │ │ ├── noteboard.css │ │ ├── notes.css │ │ ├── notification.css │ │ ├── sharer.css │ │ ├── start.css │ │ └── usernamechecker.css │ └── util │ │ ├── Promise.js │ │ └── fetchPost.js └── build │ ├── 34792c78a833f144ca514060ea3ebc41.png │ ├── 37c4047128481a7f7627690341ee9548.png │ ├── 58d6f830d3bd3ee6b64ec793797f9de2.png │ ├── bundle.js │ ├── c49873e2657b70908794d76844ce4f5f.png │ ├── d6073599a9d8e69d80b2437176aacb7a.png │ ├── d6442dfa72c816289cb78dc03824f552.png │ ├── dbce9c3bd207e839d9ac40af048f6020.png │ ├── eb158b179e77406f3c897bca05fc4b9f.svg │ └── index.html ├── config ├── config.js └── constants.js ├── package.json ├── server ├── api │ ├── Note.js │ ├── api.js │ └── db.js ├── index.js ├── server.js └── util │ └── mongo.js ├── start-api-server.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "transform-decorators-legacy", 8 | "transform-class-properties", 9 | "transform-object-rest-spread", 10 | "transform-async-to-generator", 11 | "transform-async-to-module-method", 12 | ["transform-runtime", { 13 | "polyfill": false, 14 | "regenerator": true 15 | }] 16 | ], 17 | "env": { 18 | "start": { 19 | "presets": [ 20 | "react-hrme" 21 | ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | <<<<<<< 6f87c16601e1b81e9c889801c5598faf1495fe8e 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | ======= 22 | >>>>>>> First commit - Adding the whole project 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | 42 | 43 | 44 | client/build 45 | !client/build/index.html 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 scriptify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mrnote 2 | Ever wanted your notes to be accesible from everywhere and be able to share them? Say hello to mrnote. 3 | ## In action 4 | Currently down. 5 | Just clone the repo and try it! 6 | ## Usage 7 | It's simple. 8 | On the start site, you can create a new noteboard. 9 | Give it a cool name, a not too complicated key and you are ready to go. 10 | You can decide if you want to make your board publicly available or not. 11 | What's the difference? If it's public, at entrance no key is requested. 12 | 13 | Later on, if you want to edit your board, you need to enter the correct URL: 14 | mrnote.xyz/:yourBoardName/:yourBoardPassword 15 | Now you can add, remove or edit notes. 16 | 17 | If the password is left away, the board can't be edited, but only viewed (if it's public). 18 | 19 | Now, share your board by a link! 20 | 21 | ## Stack 22 | 23 | ### Frontend 24 | - React for UI 25 | - MobX for reactive state managment 26 | - Webpack for bundling 27 | 28 | ### Backend 29 | - nodejs 30 | - express 31 | - mongodb 32 | 33 | -------------------------------------------------------------------------------- /client/app/api/api.js: -------------------------------------------------------------------------------- 1 | import fetchPost from '../util/fetchPost'; 2 | import promise from '../util/Promise'; 3 | import { SERVER_ERROR } from '../../../config/constants'; 4 | import { API_PATH, CORS, SERVER_PORT, SERVER_IP } from '../../../config/config.js'; 5 | 6 | function apiRequest(url, type = 'GET', postBody = {}) { 7 | var reqUrl = API_PATH + url; 8 | 9 | if(CORS) // API Server is external 10 | reqUrl = `http://${SERVER_IP}:${SERVER_PORT}${reqUrl}`; 11 | 12 | return request(reqUrl, type, postBody, SERVER_ERROR, CORS); 13 | } 14 | 15 | function request(url, type, postBody, error, jsonp) { 16 | 17 | if(type === 'POST') { 18 | 19 | return new promise((resolve, reject) => { 20 | fetchPost(url, postBody) 21 | .then(res => { 22 | return res.json(); 23 | }) 24 | .then(json => { 25 | if(json.err) 26 | reject(json.err); 27 | resolve(json); 28 | }) 29 | .catch(err => { 30 | console.log(err); 31 | reject(error); 32 | }); 33 | }); 34 | 35 | } else if(type === 'GET') { 36 | 37 | return new promise((resolve, reject) => { 38 | fetch(url) 39 | .then(res => { 40 | return res.json(); 41 | }) 42 | .then(json => { 43 | if(json.err) 44 | reject(json.err); 45 | resolve(json); 46 | }) 47 | .catch(err => { 48 | console.error(err); 49 | reject(error); 50 | }); 51 | }); 52 | 53 | } else { 54 | throw new Error('Invalid request type!'); 55 | } 56 | 57 | } 58 | 59 | export function create(name, password, isPublic) { 60 | const url = `create/${name}/${password}/${isPublic}`; 61 | return apiRequest(url); 62 | } 63 | 64 | export function insert(text, name, password) { 65 | const url = `insert/${name}/${password}`; 66 | return apiRequest(url, 'POST', { 67 | text 68 | }); 69 | } 70 | 71 | export function list() { 72 | return apiRequest('list'); 73 | } 74 | 75 | export function find(name, password = '') { 76 | const url = `find/${name}/${password}`; 77 | return apiRequest(url); 78 | } 79 | 80 | export function edit(name, id, password, newContent) { 81 | const url = `edit/${name}/${id}/${password}`; 82 | return apiRequest(url, 'POST', { 83 | newContent 84 | }); 85 | } 86 | 87 | export function deleteBoard(name, password) { 88 | const url = `delete/board/${name}/${password}`; 89 | return apiRequest(url); 90 | } 91 | 92 | export function deleteNote(name, password, id) { 93 | const url = `delete/note/${name}/${password}/${id}`; 94 | return apiRequest(url); 95 | } 96 | 97 | export function isUsernameAvailable(username) { 98 | return new Promise((resolve, reject) => { 99 | list() 100 | .then(users => { 101 | resolve(users.filter(user => user.name === username).length === 0); 102 | }) 103 | .catch(reject); 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /client/app/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import DevTools from 'mobx-react-devtools'; 3 | import { observer } from 'mobx-react'; 4 | import NoteBoard from './NoteBoard'; 5 | import Image from './Image'; 6 | import AppNotification from './AppNotification'; 7 | import Notification from './Notification'; 8 | import uiStore from '../stores/UiStore'; 9 | import loading from '../img/loading.svg'; 10 | 11 | 12 | 13 | @observer 14 | export default class App extends Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | } 19 | 20 | handleNotificationClose() { 21 | uiStore.notification.show = false; 22 | } 23 | 24 | render() { 25 | 26 | let notification = ''; 27 | 28 | if(uiStore.isLoading) { 29 | notification = ( 30 | 34 | 35 | 36 | ); 37 | } 38 | 39 | return ( 40 |
41 | 42 | 43 | { this.props.children } 44 | { notification } 45 |
46 | ); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /client/app/components/AppNotification.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { browserHistory } from 'react-router'; 4 | import { NAME_TAKEN, NOT_FOUND, WRONG_PW, NO_ACCESS, SERVER_ERROR, INVALID_CREDENTIALS, FORM_FIELDS_MISSING, PASSWORD_REQUIRED, TUTORIAL, APP_URL } from '../../../config/constants'; 5 | import Notification from './Notification'; 6 | import Image from './Image'; 7 | import Auth from './Auth'; 8 | import uiStore from '../stores/UiStore'; 9 | import store from '../stores/NoteBoardStore'; 10 | import notFound from '../img/notfound.png'; 11 | import continueToBoard from '../img/continue.png'; 12 | 13 | @observer 14 | export default class AppNotification extends Component { 15 | constructor(props) { 16 | super(props); 17 | } 18 | 19 | mapConstantToContent(constant) { 20 | 21 | switch(constant) { 22 | 23 | case NAME_TAKEN: 24 | return { 25 | content: 'Sorry, this name is already taken', 26 | lock: false, 27 | fixed: false 28 | }; 29 | 30 | case NOT_FOUND: 31 | return { 32 | content:

Sorry, I couldn't find the board you are looking for!

, 33 | lock: true, 34 | fixed: true 35 | }; 36 | 37 | case WRONG_PW: 38 | return { 39 | content: 40 |
41 |

This is a wrong key, argghh. Is this board really yours?

42 |
, 43 | lock: true, 44 | fixed: true 45 | }; 46 | 47 | case NO_ACCESS: 48 | return { 49 | content: { 50 | browserHistory.push(`/${store.name}/${pw}/`); 51 | window.location.reload(); // Why is this needed here? 52 | }}/>, 53 | lock: true, 54 | fixed: true 55 | }; 56 | 57 | case SERVER_ERROR: 58 | return { 59 | content: 'I think a screw is missing on my motherboard. Please try again later, I\'ll fix it as soon as possible!', 60 | lock: true, 61 | fixed: true 62 | }; 63 | 64 | case INVALID_CREDENTIALS: 65 | return { 66 | content: 'There is something wrong with your board name/key.', 67 | lock: true, 68 | fixed: false 69 | }; 70 | 71 | case FORM_FIELDS_MISSING: 72 | return { 73 | content: 'Fill in all form fields!', 74 | lock: false, 75 | fixed: false 76 | }; 77 | 78 | case PASSWORD_REQUIRED: 79 | return { 80 | content: { 81 | store.password = pw; 82 | uiStore.notification.show = false; 83 | }}/>, 84 | lock: true, 85 | fixed: true 86 | }; 87 | 88 | case TUTORIAL: 89 | return { 90 | content: 91 |
92 |

93 | Use this URL to have write access: 94 |

95 |
96 | { `${APP_URL}/${store.name}/${store.password}` } 97 |
98 |

99 | Use this URL to have read-only access: 100 |

101 |
102 | { `${APP_URL}/${store.name}/` } 103 |
104 |

Go to your board!

105 | { 106 | browserHistory.push(`/${store.name}/${store.password}/`); 107 | window.location.reload(); 108 | }}/> 109 |
, 110 | lock: true, 111 | fixed: true 112 | }; 113 | 114 | default: 115 | return { 116 | content: 'I found an unknown error, I hope this doesn\'t happen again :(', 117 | lock: true, 118 | fixed: true 119 | }; 120 | } 121 | } 122 | 123 | render() { 124 | 125 | if(!uiStore.notification.show) 126 | return
; 127 | 128 | const notification = this.mapConstantToContent(uiStore.notification.type); 129 | 130 | return ( 131 | { 135 | uiStore.notification.show = false; 136 | }} 137 | hideAfter={ notification.lock ? undefined : 2000 } 138 | > 139 | { notification.content } 140 | 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /client/app/components/Auth.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Auth extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | password: '' 8 | }; 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 |

Enter the board key, so I can let you enter :)

15 | { 16 | this.setState({ 17 | ...this.state, 18 | password: e.target.value 19 | }); 20 | }}/> 21 | { 22 | e.preventDefault(); 23 | if(this.state.password === '') 24 | return; 25 | this.props.onSubmit(this.state.password); 26 | }}/> 27 |
28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/app/components/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Checkbox extends Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return ( 11 |
12 |
13 | 14 | 15 |
16 |

{ this.props.text || '' }

17 |
18 | ); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /client/app/components/ContentEditable.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class ContentEditable extends Component { 4 | 5 | elem; 6 | 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | componentDidMount() { 12 | this.elem.textContent = this.props.content; 13 | } 14 | 15 | render() { 16 | return ( 17 |
this.elem = elem } 19 | contentEditable={ this.props.editable } 20 | onInput={this.onChange.bind(this)} 21 | onBlur={this.onChange.bind(this)} 22 | >
23 | ); 24 | } 25 | 26 | onChange(e) { 27 | this.props.onChange(this.elem.textContent); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /client/app/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import icon from '../img/icon.png'; 3 | import del from '../img/delete.png' 4 | import IconButton from './IconButton'; 5 | import Image from './Image'; 6 | 7 | export default class Header extends Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | render() { 14 | 15 | let title = ''; 16 | let onDelete = this.props.onDelete || (() => {}); 17 | 18 | if(this.props.title) { 19 | title = 20 |
23 | { this.props.title } 24 | { 25 | (this.props.onDelete && this.props.deletable) ? () : '' 26 | } 27 |
; 28 | } 29 | 30 | return ( 31 |
32 |
33 | mrnote 34 |
35 | { title } 36 |
37 | 38 |
39 |
40 | ); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /client/app/components/IconButton.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Image from './Image'; 3 | 4 | export default class IconButton extends Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | 12 | let className = 'icon-button'; 13 | let elem = 14 |
15 | 16 |
; 17 | 18 | if(this.props.className) { 19 | className += ` ${this.props.className}`; 20 | } 21 | 22 | if(this.props.hide) 23 | elem = undefined; 24 | 25 | return ( 26 |
27 | { elem } 28 |
29 | ); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /client/app/components/Image.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Image extends Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | const props = { 10 | src: this.props.src, 11 | ...this.props 12 | }; 13 | props.src = '/' + props.src; 14 | return ( 15 | 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/app/components/Note.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import IconButton from './IconButton'; 5 | import ContentEditable from './ContentEditable'; 6 | import confirm from '../img/confirm.png'; 7 | import edit from '../img/edit.png'; 8 | import del from '../img/delete.png'; 9 | 10 | @observer 11 | export default class Note extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | editing: false 16 | } 17 | } 18 | 19 | handleEditStart(e) { 20 | this.setState({ 21 | ...this.state, 22 | editing: true 23 | }); 24 | } 25 | 26 | handleContentChange(newContent) { 27 | this.props.onChange(); 28 | this.props.note.text = newContent; 29 | } 30 | 31 | handleContentConfirm(e) { 32 | this.props.onConfirmChange(); 33 | this.setState({ 34 | ...this.state, 35 | editing: false 36 | }); 37 | } 38 | 39 | render() { 40 | 41 | let text =

{ this.props.note.text }

; 42 | let actionButton = 43 | ; 44 | 45 | if(this.state.editing) { 46 | actionButton = 47 |
50 | 51 |
; 52 | 53 | text = 54 | ; 59 | } 60 | 61 | return ( 62 |
65 |
66 | { actionButton } 67 | 68 |
69 | { text } 70 |
71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /client/app/components/NoteBoard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Masonry from 'react-masonry-component'; 4 | import Header from './Header'; 5 | import Note from './Note'; 6 | import IconButton from './IconButton'; 7 | import store from '../stores/NoteBoardStore'; 8 | import newIcon from '../img/new.png'; 9 | 10 | @observer 11 | export default class NoteBoard extends Component { 12 | 13 | masonry; 14 | 15 | constructor(props) { 16 | super(props); 17 | } 18 | 19 | handleNoteSumbit(id) { 20 | store.submit(id); 21 | } 22 | 23 | handleNewNote() { 24 | store.newNote(); 25 | } 26 | 27 | handleDelete() { 28 | store.deleteBoard(); 29 | } 30 | 31 | handleNoteDelete(id) { 32 | store.deleteNote(id); 33 | } 34 | 35 | componentWillMount() { 36 | store.searchBoard(this.props.params.username, this.props.params.password); 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 |
43 |
44 | 45 |
46 | { 48 | if(!elem) 49 | return; 50 | 51 | this.masonry = elem.masonry; 52 | }} 53 | className="notes"> 54 | { 55 | store.notes.map(note => 56 | { 57 | this.handleNoteSumbit(note.id); 58 | }} 59 | onChange={() => { 60 | this.masonry.layout(); 61 | }} 62 | onDelete={e => { 63 | this.handleNoteDelete(note.id); 64 | }} 65 | /> 66 | ) 67 | } 68 | 69 |
70 | ); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /client/app/components/Notification.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import close from '../img/no.png'; 4 | import IconButton from './IconButton'; 5 | 6 | export default class Notification extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | hidden: false 11 | }; 12 | } 13 | 14 | componentDidMount() { 15 | if(this.props.hideAfter) { 16 | window.setTimeout(() => { 17 | this.setState({ 18 | ...this.state, 19 | hidden: true 20 | }); 21 | }, this.props.hideAfter) 22 | } 23 | } 24 | 25 | render() { 26 | 27 | let className = this.props.lock ? 'notification lock' : 'notification'; 28 | 29 | if(this.state.hidden || this.props.hidden === true) 30 | className += ' hidden'; 31 | 32 | return ( 33 |
34 | { (this.props.lock && !this.props.fixed) ? () : ''} 35 |
36 | { this.props.children } 37 |
38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/app/components/Sharer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Image from './Image'; 4 | 5 | import icon from '../img/icon.png'; 6 | import share from '../img/share.png'; 7 | import close from '../img/no.png'; 8 | 9 | import { 10 | ShareButtons, 11 | generateShareIcon 12 | } from 'react-share'; 13 | 14 | export default class Sharer extends Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | show: false 19 | } 20 | } 21 | 22 | handleToggle(e) { 23 | this.setState({ 24 | show: !this.state.show 25 | }); 26 | } 27 | 28 | render() { 29 | 30 | const { 31 | FacebookShareButton, 32 | GooglePlusShareButton, 33 | LinkedinShareButton, 34 | TwitterShareButton, 35 | PinterestShareButton, 36 | VKShareButton 37 | } = ShareButtons; 38 | 39 | const FacebookIcon = generateShareIcon('facebook'); 40 | const TwitterIcon = generateShareIcon('twitter'); 41 | const GooglePlusIcon = generateShareIcon('google'); 42 | const LinkedinIcon = generateShareIcon('linkedin'); 43 | const PinterestIcon = generateShareIcon('pinterest'); 44 | const VKIcon = generateShareIcon('vk'); 45 | 46 | const title = 'mrnote - notes everywhere, for everyone'; 47 | const description = 'mrnote - your notes always available, on every device, for everyone. It\'s that easy. Create a new noteboard now, it will take you 2 seconds!'; 48 | const url = 'http://www.mrnote.xyz'; 49 | const img = url + '/' + icon; 50 | 51 | let content = ''; 52 | let navIcon = share; 53 | 54 | 55 | if(this.state.show) { 56 | navIcon = close; 57 | content = 58 |
59 |
} description={ description } title={ title } url={ url } /> 60 | } description={ description } title={ title } url={ url } /> 61 | } description={ description } title={ title } url={ url } /> 62 | } description={ description } title={ title } url={ url } /> 63 | } description={ description } title={ title } url={ url } /> 64 | } description={ description } title={ title } url={ url } /> 65 | ; 66 | } 67 | 68 | 69 | 70 | return ( 71 |
72 | 73 | { content } 74 |
75 | ); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /client/app/components/Start.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Sharer from './Sharer'; 3 | import Checkbox from './Checkbox'; 4 | import Header from './Header'; 5 | import Image from './Image'; 6 | 7 | import gitlogo from '../img/git-logo.png'; 8 | 9 | import UsernameChecker from './UsernameChecker'; 10 | import uiStore from '../stores/UiStore'; 11 | 12 | import { FORM_FIELDS_MISSING, NAME_TAKEN, MAX_CHARS } from '../../../config/constants'; 13 | 14 | export default class Start extends Component { 15 | 16 | currUsernameCheck = '' 17 | 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | usernameAvailable: undefined, 22 | username: '', 23 | password: '', 24 | isPublic: true, 25 | }; 26 | } 27 | 28 | checkAvailability(username) { 29 | this.currUsernameCheck = username; 30 | uiStore.isUsernameAvailable(username) 31 | .then(isAvailable => { 32 | 33 | if(this.currUsernameCheck === username) { 34 | this.setState({ 35 | ...this.state, 36 | usernameAvailable: isAvailable ? 1 : 0 37 | }); 38 | } 39 | 40 | }) 41 | .catch(err => { 42 | uiStore.setNotification(err); 43 | }); 44 | } 45 | 46 | handleCreate(e) { 47 | e.preventDefault(); 48 | 49 | if(this.state.username === '' || this.state.password === '') { 50 | // ERROR! 51 | uiStore.setNotification(FORM_FIELDS_MISSING); 52 | return; 53 | } 54 | 55 | if(this.state.usernameAvailable !== 1) { 56 | // ERROR 57 | uiStore.setNotification(NAME_TAKEN); 58 | return; 59 | } 60 | 61 | uiStore.createBoard(this.state.username, this.state.password, this.state.isPublic); 62 | } 63 | 64 | handleUserNameChange(e) { 65 | const val = 66 | e.target.value 67 | .replace(/[^0-9a-z]/gi, '-') 68 | .toLowerCase(); 69 | 70 | this.setState({ 71 | ...this.state, 72 | username: val 73 | }, () => { 74 | this.checkAvailability(this.state.username); 75 | }); 76 | } 77 | 78 | handlePasswordChange(e) { 79 | 80 | const val = 81 | e.target.value 82 | .replace(/[^0-9a-z]/gi, '-') 83 | .toLowerCase(); 84 | 85 | this.setState({ 86 | ...this.state, 87 | password: val 88 | }); 89 | } 90 | 91 | handlePublicChange() { 92 | this.setState({ 93 | ...this.state, 94 | isPublic: !this.state.isPublic 95 | }); 96 | } 97 | 98 | render() { 99 | 100 | return ( 101 |
102 |
103 |
104 |
105 | 106 | 0) ? this.state.usernameAvailable : undefined } 108 | onChange={ this.handleUserNameChange.bind(this) } 109 | /> 110 | 111 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
125 |
126 | ); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /client/app/components/UsernameChecker.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import check from '../img/confirm.png'; 3 | import no from '../img/no.png'; 4 | import Image from './IconButton'; 5 | 6 | export default class UsernameChecker extends Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | 13 | let icon = ''; 14 | if(this.props.available === 0) 15 | icon = 16 | else if(this.props.available === 1) 17 | icon = 18 | else if(this.props.available === -1) 19 | icon = 20 | 21 | return ( 22 |
23 | 27 | { icon } 28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/app/img/confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/app/img/confirm.png -------------------------------------------------------------------------------- /client/app/img/continue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/app/img/continue.png -------------------------------------------------------------------------------- /client/app/img/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/app/img/delete.png -------------------------------------------------------------------------------- /client/app/img/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/app/img/edit.png -------------------------------------------------------------------------------- /client/app/img/git-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/app/img/git-logo.png -------------------------------------------------------------------------------- /client/app/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/app/img/icon.png -------------------------------------------------------------------------------- /client/app/img/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 28 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /client/app/img/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/app/img/new.png -------------------------------------------------------------------------------- /client/app/img/no.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/app/img/no.png -------------------------------------------------------------------------------- /client/app/img/notfound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/app/img/notfound.png -------------------------------------------------------------------------------- /client/app/img/retry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/app/img/retry.png -------------------------------------------------------------------------------- /client/app/img/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/app/img/share.png -------------------------------------------------------------------------------- /client/app/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Router, Route, Link, browserHistory, IndexRoute } from 'react-router'; 4 | 5 | import 'whatwg-fetch'; // Fetch polyfill 6 | 7 | import App from './components/App'; 8 | import Start from './components/Start'; 9 | import NoteBoard from './components/NoteBoard'; 10 | 11 | import './style/global.css'; 12 | import './style/header.css'; 13 | import './style/start.css'; 14 | import './style/note.css'; 15 | import './style/notes.css'; 16 | import './style/noteboard.css'; 17 | import './style/usernamechecker.css'; 18 | import './style/checkbox.css'; 19 | import './style/notification.css'; 20 | import './style/sharer.css'; 21 | 22 | 23 | import * as api from './api/api'; 24 | 25 | render( 26 | 27 | 28 | 29 | 30 | 31 | , 32 | document.getElementById('app') 33 | ); 34 | -------------------------------------------------------------------------------- /client/app/models/Note.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | 3 | export default class Note { 4 | id; 5 | @observable text; 6 | 7 | constructor(text, id = '') { 8 | this.text = text; 9 | this.id = id; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /client/app/models/Notification.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | 3 | export default class Notification { 4 | @observable type; 5 | @observable show = false; 6 | } 7 | -------------------------------------------------------------------------------- /client/app/stores/NoteBoardStore.js: -------------------------------------------------------------------------------- 1 | import { observable, computed } from 'mobx'; 2 | import { find, edit, insert, deleteBoard, deleteNote } from '../api/api'; 3 | import Note from '../models/Note'; 4 | import uiStore from './UiStore'; 5 | 6 | class NoteBoardStore { 7 | @observable name; 8 | @observable notes = []; 9 | @observable password; 10 | @computed get isAuthenticated() { 11 | return (this.password && this.password !== '') 12 | } 13 | 14 | deleteNote(id) { 15 | uiStore.setLoading(); 16 | deleteNote(this.name, this.password, id) 17 | .then(() => { 18 | this.notes.replace(this.notes.filter(note => note.id !== id)); 19 | uiStore.unsetLoading(); 20 | }) 21 | .catch(err => { 22 | uiStore.unsetLoading(); 23 | uiStore.setNotification(err); 24 | }); 25 | } 26 | 27 | deleteBoard() { 28 | uiStore.setLoading(); 29 | deleteBoard(this.name, this.password) 30 | .then(() => { 31 | uiStore.unsetLoading(); 32 | window.location.href = '/'; 33 | }) 34 | .catch(err => { 35 | uiStore.unsetLoading(); 36 | uiStore.setNotification(err); 37 | }); 38 | } 39 | 40 | searchBoard(name, password) { 41 | this.name = name; 42 | this.password = password; 43 | this.find(); 44 | } 45 | 46 | submit(id) { 47 | 48 | 49 | // Submit note to server 50 | uiStore.setLoading(); 51 | const { text } = this.notes.filter(note => note.id === id)[0]; 52 | 53 | edit(this.name, id, this.password, text) 54 | .then(() => { 55 | uiStore.unsetLoading(); 56 | }) 57 | .catch(err => { 58 | uiStore.unsetLoading(); 59 | uiStore.setNotification(err); 60 | }); 61 | } 62 | 63 | newNote() { 64 | uiStore.setLoading(); 65 | const note = new Note('New note...'); 66 | insert(note.text, this.name, this.password) 67 | .then(ret => { 68 | uiStore.unsetLoading(); 69 | note.id = ret.id; 70 | this.notes.unshift(note); // Add at array beginning 71 | }) 72 | .catch(err => { 73 | uiStore.unsetLoading(); 74 | uiStore.setNotification(err); 75 | }); 76 | } 77 | 78 | find() { 79 | uiStore.setLoading(true); 80 | find(this.name, this.password) 81 | .then(board => { 82 | board.notes.forEach(note => 83 | this.notes.push(new Note(note.text, note.id)) 84 | ); 85 | uiStore.unsetLoading(); 86 | }) 87 | .catch(err => { 88 | uiStore.setNotification(err); 89 | uiStore.unsetLoading(); 90 | }); 91 | } 92 | 93 | } 94 | 95 | const store = new NoteBoardStore(); 96 | export default store; 97 | -------------------------------------------------------------------------------- /client/app/stores/UiStore.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | import { browserHistory } from 'react-router'; 3 | 4 | import Notification from '../models/Notification'; 5 | 6 | import { isUsernameAvailable, create } from '../api/api'; 7 | 8 | import { TUTORIAL } from '../../../config/constants'; 9 | 10 | import notesStore from './NoteBoardStore'; 11 | 12 | class UiStore { 13 | @observable isLoading = false; 14 | @observable notification = new Notification(); 15 | 16 | isUsernameAvailable(username) { 17 | return isUsernameAvailable(username); 18 | } 19 | 20 | createBoard(username, password, isPublic) { 21 | this.setLoading(true); 22 | create(username, password, isPublic) 23 | .then(res => { 24 | notesStore.name = username; 25 | notesStore.password = password; 26 | this.unsetLoading(); 27 | this.setNotification(TUTORIAL); 28 | }) 29 | .catch(err => { 30 | this.unsetLoading(); 31 | this.setNotification(err); 32 | }); 33 | } 34 | 35 | setNotification(constant) { 36 | this.notification.type = constant; 37 | this.notification.show = true; 38 | } 39 | 40 | setLoading(shouldLock) { 41 | this.isLoading = true; 42 | this.lockLoading = shouldLock; 43 | } 44 | 45 | unsetLoading() { 46 | this.isLoading = false; 47 | } 48 | 49 | } 50 | 51 | const store = new UiStore(); 52 | 53 | export default store; 54 | -------------------------------------------------------------------------------- /client/app/style/checkbox.css: -------------------------------------------------------------------------------- 1 | 2 | .checkbox { 3 | margin-top: 10px; 4 | 5 | & p { 6 | color: #FFF; 7 | font-style: italic; 8 | } 9 | 10 | } 11 | 12 | .squaredFour { 13 | width: 20px; 14 | position: relative; 15 | margin: 20px auto; 16 | } 17 | .squaredFour label { 18 | width: 20px; 19 | height: 20px; 20 | cursor: pointer; 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | background: #fcfff4; 25 | background: -webkit-linear-gradient(top, #fcfff4 0%, #dfe5d7 40%, #b3bead 100%); 26 | background: linear-gradient(to bottom, #fcfff4 0%, #dfe5d7 40%, #b3bead 100%); 27 | border-radius: 4px; 28 | box-shadow: inset 0px 1px 1px white, 0px 1px 3px rgba(0, 0, 0, 0.5); 29 | } 30 | .squaredFour label:after { 31 | content: ''; 32 | width: 9px; 33 | height: 5px; 34 | position: absolute; 35 | top: 4px; 36 | left: 4px; 37 | border: 3px solid #333; 38 | border-top: none; 39 | border-right: none; 40 | background: transparent; 41 | opacity: 0; 42 | -webkit-transform: rotate(-45deg); 43 | transform: rotate(-45deg); 44 | } 45 | .squaredFour label:hover::after { 46 | opacity: 0.5; 47 | } 48 | .squaredFour input[type=checkbox] { 49 | visibility: hidden; 50 | } 51 | .squaredFour input[type=checkbox]:checked + label:after { 52 | opacity: 1; 53 | } 54 | -------------------------------------------------------------------------------- /client/app/style/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing:border-box; 5 | } 6 | 7 | body { 8 | font-family: 'Calibri', 'Roboto', sans-serif; 9 | background-color: #045FB4; 10 | } 11 | 12 | .icon-button { 13 | height: 22px; 14 | width: 22px; 15 | padding: 3px; 16 | border-radius: 20px; 17 | } 18 | 19 | .icon-button img { 20 | height: 100%; 21 | width: 100%; 22 | } 23 | -------------------------------------------------------------------------------- /client/app/style/header.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --padding: 20px; 3 | --infoFontSize: 1em; 4 | } 5 | 6 | header { 7 | 8 | text-align: center; 9 | padding: var(--padding); 10 | background-color: #2E9AFE; 11 | color: #FFFFFF; 12 | 13 | & div { 14 | display: inline-block; 15 | } 16 | 17 | & .logo, & .name { 18 | font-size: var(--infoFontSize); 19 | position: absolute; 20 | top: var(--padding); 21 | } 22 | 23 | & .name { 24 | left: var(--padding); 25 | } 26 | 27 | & .title { 28 | font-size: 2em; 29 | margin-top: var(--infoFontSize); 30 | } 31 | 32 | & .logo { 33 | right: 5px; 34 | top: 10px; 35 | padding-left: 18px; 36 | padding-right: 18px; 37 | padding-top: 2px; 38 | padding-bottom: 2px; 39 | } 40 | 41 | & .logo img { 42 | height: 4em; 43 | } 44 | 45 | & .delete-button { 46 | margin-left: 10px; 47 | cursor: pointer; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/app/style/note.css: -------------------------------------------------------------------------------- 1 | 2 | .note { 3 | padding: 5px; 4 | border: solid 1px black; 5 | max-width: 500px; 6 | width: 50%; 7 | padding: 10px; 8 | color: #FFF; 9 | overflow: hidden; 10 | min-height: 100px; 11 | 12 | & div[contentEditable="true"] { 13 | box-shadow: 0px 0px 10px black; 14 | } 15 | 16 | & .button-bar { 17 | width: 100%; 18 | height: 25px; 19 | } 20 | 21 | & .action-button { 22 | background-color: #2E9AFE; 23 | cursor: pointer; 24 | float: right; 25 | } 26 | 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /client/app/style/noteboard.css: -------------------------------------------------------------------------------- 1 | .board { 2 | & .button-bar { 3 | width: 100%; 4 | padding: 10px; 5 | text-align: center; 6 | 7 | & .new-button { 8 | margin: 0 auto; 9 | } 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/app/style/notes.css: -------------------------------------------------------------------------------- 1 | 2 | .notes { 3 | padding: 0; 4 | } 5 | -------------------------------------------------------------------------------- /client/app/style/notification.css: -------------------------------------------------------------------------------- 1 | 2 | .notification { 3 | position: fixed; 4 | top: 0; 5 | right: 0; 6 | padding: 10px; 7 | background-color: #848484; 8 | border-bottom-left-radius: 10px; 9 | color: #FFF; 10 | z-index: 100; 11 | 12 | & .tutorial { 13 | & .description { 14 | margin-bottom: 10px; 15 | } 16 | 17 | & .url { 18 | font-size: 13pt; 19 | margin-bottom: 10px; 20 | padding: 5px; 21 | border: dashed 1px #FFF; 22 | } 23 | } 24 | 25 | } 26 | 27 | .notification.lock { 28 | width: 100%; 29 | height: 100%; 30 | background-color: rgba(4, 95, 180, 1); 31 | 32 | & .close { 33 | position: relative; 34 | top: 0; 35 | right: 0; 36 | background-color: #FFF; 37 | z-index: 1000; 38 | } 39 | 40 | } 41 | 42 | .notification.lock > .content { 43 | position: absolute; 44 | top: 50%; 45 | left: 50%; 46 | transform: translate(-50%, -50%); 47 | text-align: center; 48 | font-size: 1.4em; 49 | } 50 | 51 | .notification.hidden { 52 | animation: fadeOut 0.5s; 53 | visibility: hidden; 54 | } 55 | 56 | @keyframes fadeOut { 57 | from { 58 | opacity: 1; 59 | visibility: visible; 60 | } to { 61 | opacity: 0; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /client/app/style/sharer.css: -------------------------------------------------------------------------------- 1 | .sharer { 2 | position: fixed; 3 | left: 10px; 4 | bottom: 10px; 5 | padding: 2px; 6 | 7 | & .icon * { 8 | border-radius: 100%; 9 | margin-bottom: 2px; 10 | } 11 | 12 | & .share-icon { 13 | height: 50px; 14 | } 15 | 16 | } 17 | 18 | @media screen and (orientation:landscape) { 19 | .sharer { 20 | & .icon { 21 | display: inline-block; 22 | height: 0; 23 | margin-bottom: 0; 24 | margin-right: 2px; 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /client/app/style/start.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --padding: 20px; 3 | } 4 | 5 | ::-webkit-input-placeholder { /* WebKit, Blink, Edge */ 6 | color: #848484; 7 | font-style: italic; 8 | font-size: 0.8em; 9 | } 10 | :-moz-placeholder { /* Mozilla Firefox 4 to 18 */ 11 | color: #848484; 12 | font-style: italic; 13 | font-size: 0.8em; 14 | } 15 | ::-moz-placeholder { /* Mozilla Firefox 19+ */ 16 | color: #848484; 17 | font-style: italic; 18 | font-size: 0.8em; 19 | } 20 | :-ms-input-placeholder { /* Internet Explorer 10-11 */ 21 | color: #848484; 22 | font-style: italic; 23 | font-size: 0.8em; 24 | } 25 | 26 | .start { 27 | text-align: center; 28 | padding-top: var(--padding); 29 | padding-bottom: var(--padding); 30 | height: 100%; 31 | min-height: 500px; 32 | background: url(../img/icon.png) center center; 33 | background-repeat: no-repeat; 34 | background-size: auto 100%; 35 | 36 | & input { 37 | display: block; 38 | margin: 0px auto; 39 | } 40 | 41 | & input.margin { 42 | margin-top: var(--padding); 43 | } 44 | 45 | & .text-field, & input[type="submit"] { 46 | background-color: #5BB0FF; 47 | font-size: 1.4em; 48 | border: none; 49 | border-radius: 4px; 50 | text-align: center; 51 | box-shadow: none; 52 | } 53 | 54 | & .text-field { 55 | padding: 10px; 56 | color: #045FB4; 57 | margin-bottom: 3px; 58 | } 59 | 60 | & input[type="submit"] { 61 | padding: var(--padding); 62 | color: #FFF; 63 | } 64 | 65 | & input[type="submit"]:hover, 66 | & input[type="submit"]:active, 67 | & input[type="submit"]:focus { 68 | box-shadow: inset 0px 1px 10px 0px #000; 69 | } 70 | 71 | & .git-logo { 72 | position: fixed; 73 | right: 20px; 74 | bottom: 20px; 75 | height: 100px; 76 | width: 100px; 77 | box-shadow: 0px 0px 20px #000; 78 | border-radius: 100px; 79 | } 80 | 81 | & .git-logo:hover, 82 | & .git-logo:active, 83 | & .git-logo:focus { 84 | box-shadow: 0px 0px 50px #000; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /client/app/style/usernamechecker.css: -------------------------------------------------------------------------------- 1 | .username-checker { 2 | & * { 3 | display: inline-block; 4 | } 5 | 6 | & .checker-icon { 7 | margin-top: 10px; 8 | background-color: #FFF; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/app/util/Promise.js: -------------------------------------------------------------------------------- 1 | import es6Promise from 'es6-promise'; 2 | const promise = window.Promise || global.Promise || es6Promise.polyfill(); 3 | 4 | export default promise; 5 | -------------------------------------------------------------------------------- /client/app/util/fetchPost.js: -------------------------------------------------------------------------------- 1 | export default function fetchPost(url, json, cors) { 2 | 3 | return fetch(url, { 4 | method: 'POST', 5 | headers: { 6 | 'Accept': 'application/json', 7 | 'content-type': 'application/json' 8 | }, 9 | body: JSON.stringify(json) 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /client/build/34792c78a833f144ca514060ea3ebc41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/build/34792c78a833f144ca514060ea3ebc41.png -------------------------------------------------------------------------------- /client/build/37c4047128481a7f7627690341ee9548.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/build/37c4047128481a7f7627690341ee9548.png -------------------------------------------------------------------------------- /client/build/58d6f830d3bd3ee6b64ec793797f9de2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/build/58d6f830d3bd3ee6b64ec793797f9de2.png -------------------------------------------------------------------------------- /client/build/c49873e2657b70908794d76844ce4f5f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/build/c49873e2657b70908794d76844ce4f5f.png -------------------------------------------------------------------------------- /client/build/d6073599a9d8e69d80b2437176aacb7a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/build/d6073599a9d8e69d80b2437176aacb7a.png -------------------------------------------------------------------------------- /client/build/d6442dfa72c816289cb78dc03824f552.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/build/d6442dfa72c816289cb78dc03824f552.png -------------------------------------------------------------------------------- /client/build/dbce9c3bd207e839d9ac40af048f6020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptify/mrnote/034612b6ed6fa01a421d43a4f538bba249629f1b/client/build/dbce9c3bd207e839d9ac40af048f6020.png -------------------------------------------------------------------------------- /client/build/eb158b179e77406f3c897bca05fc4b9f.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | mrnote 5 | 6 | 7 | 8 |
9 | 10 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const DEVELOPMENT = 'development'; 4 | const PRODUCTION = 'production'; 5 | 6 | function getEnvironment() { 7 | if(typeof window !== 'undefined') { 8 | return ENVIRONMENT; 9 | } else { 10 | return (process.env.npm_lifecycle_event === 'dev') ? DEVELOPMENT : PRODUCTION; 11 | } 12 | } 13 | 14 | const ENV = getEnvironment(); 15 | 16 | console.log('Config in ' + ENV + ' mode'); 17 | 18 | module.exports = { 19 | PUBLIC_PATH: path.join(__dirname, '../client/build'), 20 | APP_PATH: path.join(__dirname, '../client/app'), 21 | IMG_PATH: path.join(__dirname, '../client/app/img'), 22 | CONFIG_PATH: path.join(__dirname, '.'), 23 | DB_URL: 'mongodb://localhost:27017/noteboard', 24 | API_PATH: '/api/', 25 | SERVER_PORT: (ENV === DEVELOPMENT) ? 3000 : 8080, 26 | SERVER_IP: '127.0.0.1', 27 | CORS: (ENV === DEVELOPMENT) 28 | }; 29 | -------------------------------------------------------------------------------- /config/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NAME_TAKEN: 'NAME_TAKEN', 3 | NOT_FOUND: 'NOT_FOUND', 4 | WRONG_PW: 'WRONG_PW', 5 | GREETING_NOTE_MSG: 'Welcome! This is my first note.', 6 | NO_ACCESS: 'NO_ACCESS', 7 | SERVER_ERROR: 'SERVER_ERROR', 8 | INVALID_CREDENTIALS: 'INVALID_CREDENTIALS', 9 | FORM_FIELDS_MISSING: 'FORM_FIELDS_MISSING', 10 | PASSWORD_REQUIRED: 'PASSWORD_REQUIRED', 11 | APP_URL: 'mrnote.xyz', 12 | MAX_CHARS: 20 // For both board name and key 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-notes", 3 | "version": "1.0.0", 4 | "description": "Ever wanted your notes to be accesible from everywhere and be able to share them? Say hello to mrnote.", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "dev": "node start-api-server.js & webpack-dev-server", 8 | "start": "webpack && node start-api-server.js" 9 | }, 10 | "keywords": [], 11 | "author": "Maximilian Torggler", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "babel-core": "^6.9.1", 15 | "babel-loader": "^6.2.4", 16 | "babel-plugin-transform-async-to-generator": "^6.8.0", 17 | "babel-plugin-transform-async-to-module-method": "^6.8.0", 18 | "babel-plugin-transform-class-properties": "^6.9.1", 19 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 20 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 21 | "babel-plugin-transform-runtime": "^6.9.0", 22 | "babel-polyfill": "^6.9.1", 23 | "babel-preset-es2015": "^6.9.0", 24 | "css-loader": "^0.23.1", 25 | "image-webpack-loader": "^1.8.0", 26 | "mobx-react-devtools": "^4.2.0", 27 | "npm-install-webpack-plugin": "^4.0.1", 28 | "pm2": "^1.1.3", 29 | "postcss-cssnext": "^2.6.0", 30 | "postcss-loader": "^0.9.1", 31 | "style-loader": "^0.13.1", 32 | "webpack": "^1.13.1", 33 | "webpack-dev-server": "^1.14.1", 34 | "webpack-merge": "^0.14.0", 35 | "worker-loader": "^0.7.0" 36 | }, 37 | "dependencies": { 38 | "babel-preset-react": "^6.5.0", 39 | "body-parser": "*", 40 | "compression": "*", 41 | "cors": "*", 42 | "credentials": "^2.0.0", 43 | "es6-promise": "^3.2.1", 44 | "express": "*", 45 | "file-loader": "^0.8.5", 46 | "mobx": "^2.3.1", 47 | "mobx-react": "^3.3.1", 48 | "mongodb": "^2.1.18", 49 | "react": "^15.1.0", 50 | "react-dom": "^15.1.0", 51 | "react-masonry-component": "^4.0.4", 52 | "react-router": "^2.4.1", 53 | "react-share": "^1.8.4", 54 | "uuid": "*", 55 | "whatwg-fetch": "*" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/scriptify/mrnote.git" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/scriptify/mrnote/issues" 63 | }, 64 | "homepage": "https://github.com/scriptify/mrnote#readme" 65 | } 66 | -------------------------------------------------------------------------------- /server/api/Note.js: -------------------------------------------------------------------------------- 1 | const { v4 } = require('uuid'); 2 | 3 | function createNote(text) { 4 | return { 5 | id: v4(), 6 | text, 7 | uploaded: Date.now() 8 | }; 9 | } 10 | 11 | module.exports = createNote; 12 | -------------------------------------------------------------------------------- /server/api/api.js: -------------------------------------------------------------------------------- 1 | const MongoAccess = require('./db'); 2 | const { INVALID_CREDENTIALS, MAX_CHARS } = require('../../config/constants'); 3 | 4 | class NotesAPI { 5 | 6 | constructor(collection) { 7 | this.db = new MongoAccess(collection); 8 | } 9 | 10 | mrnoteValidate(input) { 11 | return input 12 | .substring(0, MAX_CHARS) 13 | .replace(/[^0-9a-z]/gi, '-') 14 | .toLowerCase(); 15 | } 16 | 17 | edit(name, id, password, newContent) { 18 | return new Promise((resolve, reject) => { 19 | this.db.auth(name, password) 20 | .then(() => { 21 | this.db.edit(name, id, newContent) 22 | .then(resolve) 23 | .catch(reject); 24 | }) 25 | .catch(reject); 26 | }); 27 | } 28 | 29 | create(name, password, isPublic) { 30 | 31 | return new Promise((resolve, reject) => { 32 | 33 | name = this.mrnoteValidate(name); 34 | password = this.mrnoteValidate(password); 35 | 36 | if(name === '' || password === '') 37 | reject(INVALID_CREDENTIALS) 38 | 39 | this.db.create(name, password, isPublic) 40 | .then(resolve) 41 | .catch(reject); 42 | 43 | }); 44 | 45 | } 46 | 47 | insert(text, name, password) { 48 | 49 | return new Promise((resolve, reject) => { 50 | this.db.auth(name, password) 51 | .then(() => { 52 | this.db.insert(text, name) 53 | .then(resolve) 54 | .catch(reject); 55 | }) 56 | .catch(reject); 57 | }); 58 | 59 | } 60 | 61 | list() { 62 | return new Promise((resolve, reject) => { 63 | this.db.list() 64 | .then(resolve) 65 | .catch(reject); 66 | }); 67 | } 68 | 69 | find(name, password) { 70 | return new Promise((resolve, reject) => { 71 | this.db.find(name, password) 72 | .then(resolve) 73 | .catch(reject); 74 | }); 75 | } 76 | 77 | deleteBoard(name, password) { 78 | return new Promise((resolve, reject) => { 79 | this.db.auth(name, password) 80 | .then(() => { 81 | this.db.deleteBoard(name) 82 | .then(resolve) 83 | .catch(reject); 84 | }) 85 | .catch(reject); 86 | }); 87 | } 88 | 89 | deleteNote(name, password, noteId) { 90 | return new Promise((resolve, reject) => { 91 | this.db.auth(name, password) 92 | .then(() => { 93 | this.db.deleteNote(name, noteId) 94 | .then(resolve) 95 | .catch(reject); 96 | }) 97 | .catch(reject); 98 | }); 99 | } 100 | 101 | } 102 | 103 | module.exports = NotesAPI; 104 | -------------------------------------------------------------------------------- /server/api/db.js: -------------------------------------------------------------------------------- 1 | const credentials = require('credentials')(); const createNote = 2 | require('./Note'); const { NAME_TAKEN, NOT_FOUND, WRONG_PW, 3 | GREETING_NOTE_MSG, NO_ACCESS } = require('../../config/constants'); class 4 | MongoAccess { 5 | constructor(collection) { 6 | this.collection = collection; 7 | } 8 | edit(name, id, newContent) { 9 | return new Promise((resolve, reject) => { 10 | this.collection.updateOne( 11 | { name }, 12 | { $pull: { notes: { id } } }, 13 | (err, result) => { 14 | if(err) 15 | reject(err); 16 | this.insert(newContent, name) 17 | .then(resolve) 18 | .catch(reject); 19 | }); 20 | }); 21 | } 22 | create(name, password, isPublic) { 23 | return new Promise((resolve, reject) => { 24 | // Lookup if it already exists 25 | this.find(name) 26 | .then(doc => { 27 | reject(NAME_TAKEN); 28 | }) 29 | .catch(err => { 30 | if(err !== NOT_FOUND) 31 | reject(err); 32 | credentials.hash(password) 33 | .then(pwHash => { 34 | this.collection.insert({ 35 | name, 36 | pwHash, 37 | isPublic, 38 | notes: [createNote(GREETING_NOTE_MSG)] 39 | }, function(err, result) { 40 | if(err) { 41 | reject(err); 42 | } 43 | resolve(); 44 | }); 45 | }) 46 | .catch(reject); 47 | }); 48 | }); 49 | } 50 | insert(text, name) { 51 | return new Promise((resolve, reject) => { 52 | const note = createNote(text); 53 | this.collection.updateOne({ 54 | name 55 | }, { 56 | $push: { 57 | notes: { 58 | $each: [ note ], 59 | $sort: { uploaded: -1 } 60 | } 61 | } 62 | }, function(err, result) { 63 | if(err) { 64 | reject(err); 65 | } 66 | resolve(note.id); 67 | }); 68 | }); 69 | } 70 | list() { 71 | return new Promise((resolve, reject) => { 72 | this.collection.find({}, {name: 1}).toArray((err, docs) => { 73 | if(err) { 74 | reject(err); 75 | } 76 | resolve(docs); 77 | }); 78 | }); 79 | } 80 | find(name, password = '') { 81 | return new Promise((resolve, reject) => { 82 | this.collection.findOne({name}) 83 | .then(doc => { 84 | if(!doc) 85 | reject(NOT_FOUND); 86 | if(!JSON.parse(doc.isPublic)) { 87 | if(password === '' || !password) 88 | reject(NO_ACCESS); 89 | this.authAndFind(name, password, doc) 90 | .then(resolve) 91 | .catch(reject); 92 | } else if(password !== '') { 93 | this.authAndFind(name, password, doc) 94 | .then(resolve) 95 | .catch(reject); 96 | } else { 97 | this.payloadOnly(doc); 98 | resolve(doc); 99 | } 100 | }) 101 | .catch(reject); 102 | }); 103 | } 104 | payloadOnly(doc) { 105 | delete doc["pwHash"]; 106 | delete doc["_id"]; 107 | } 108 | authAndFind(name, password, doc) { 109 | return new Promise((resolve, reject) => { 110 | this.auth(name, password, doc.pwHash) 111 | .then(() => { 112 | this.payloadOnly(doc); 113 | resolve(doc); 114 | }) 115 | .catch(reject); 116 | }); 117 | } 118 | deleteBoard(name) { 119 | return new Promise((resolve, reject) => { 120 | this.collection.deleteOne({name}) 121 | .then(resolve) 122 | .catch(reject); 123 | }); 124 | } 125 | deleteNote(boardName, noteId) { 126 | return new Promise((resolve, reject) => { 127 | this.collection.updateOne({name: boardName}, { 128 | $pull: { 129 | notes: { id: noteId } 130 | } 131 | }) 132 | .then(resolve) 133 | .catch(reject); 134 | }); 135 | } 136 | auth(name, password, pwHash) { 137 | 138 | return new Promise((resolve, reject) => { 139 | 140 | const verify = (hash) => { 141 | if(password === '') { 142 | reject(NO_ACCESS); 143 | return; 144 | } 145 | credentials.verify(hash, password) 146 | .then(isValid => { 147 | if(!isValid) { 148 | reject(WRONG_PW); 149 | } 150 | resolve(); 151 | }); 152 | } 153 | if(!pwHash) { 154 | this.collection.findOne({name}) 155 | .then(doc => { 156 | if(!doc) 157 | reject(NOT_FOUND); 158 | verify(doc.pwHash); 159 | }) 160 | .catch(reject); 161 | } else { 162 | verify(pwHash); 163 | } 164 | }); 165 | } 166 | } 167 | module.exports = MongoAccess; 168 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const { DB_URL } = require('../config/config.js'); 2 | const { connect } = require('./util/mongo.js'); 3 | const server = require('./server'); 4 | 5 | connect(DB_URL) 6 | .then(db => { 7 | server(db); 8 | }) 9 | .catch(err => { 10 | console.log('A server error occured: ', err); 11 | }); 12 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const compression = require('compression'); 4 | const bodyParser = require('body-parser'); 5 | const cors = require('cors'); 6 | const NoteAPI = require('./api/api'); 7 | const { PUBLIC_PATH, SERVER_PORT, SERVER_IP } = require('../config/config.js'); 8 | 9 | const sendRes = (res, content) => { 10 | res.json(content); 11 | } 12 | 13 | module.exports = function(db) { 14 | 15 | const collection = db.collection('notes'); 16 | 17 | const api = new NoteAPI(collection); 18 | 19 | const app = express(); 20 | 21 | app.use(bodyParser.json()); 22 | app.use(compression()); 23 | app.use(cors()); 24 | 25 | app.use(express.static(PUBLIC_PATH)); 26 | 27 | app.post('/api/edit/:name/:id/:password/', (req, res) => { 28 | const { name, id, password } = req.params; 29 | const { newContent } = req.body; 30 | 31 | api.edit(name, id, password, newContent) 32 | .then(() => { 33 | sendRes(res, {msg: 'SUCCESS'}); 34 | }) 35 | .catch(err => { 36 | sendRes(res, {err}); 37 | }) 38 | }); 39 | 40 | app.get('/api/create/:name/:password/:isPublic', (req, res) => { 41 | const { name, password, isPublic } = req.params; 42 | 43 | api.create(name, password, isPublic) 44 | .then(() => { 45 | sendRes(res, {msg: 'SUCCESS'}); 46 | }) 47 | .catch(err => { 48 | sendRes(res, {err}); 49 | }); 50 | }); 51 | 52 | app.post('/api/insert/:name/:password', (req, res) => { 53 | const { name, password } = req.params; 54 | const { text } = req.body; 55 | 56 | api.insert(text, name, password) 57 | .then(id => { 58 | sendRes(res, {msg: 'SUCCESS', id}); 59 | }) 60 | .catch(err => { 61 | sendRes(res, {err}); 62 | }); 63 | }); 64 | 65 | app.get('/api/list', (req, res) => { 66 | api.list() 67 | .then(items => { 68 | sendRes(res, items); 69 | }) 70 | .catch(err => { 71 | sendRes(res, {err}); 72 | }); 73 | }); 74 | 75 | app.get('/api/find/:name/:password?', (req, res) => { 76 | const { name, password } = req.params; 77 | api.find(name, password) 78 | .then(notes => { 79 | sendRes(res, notes); 80 | }) 81 | .catch(err => { 82 | sendRes(res, {err}); 83 | }); 84 | }); 85 | 86 | app.get('/api/delete/board/:name/:password', (req, res) => { 87 | const { name, password } = req.params; 88 | api.deleteBoard(name, password) 89 | .then(data => { 90 | sendRes(res, {msg: 'SUCCESS'}); 91 | }) 92 | .catch(err => { 93 | sendRes(res, {err}); 94 | }); 95 | }); 96 | 97 | app.get('/api/delete/note/:name/:password/:noteId', (req, res) => { 98 | const { name, password, noteId } = req.params; 99 | api.deleteNote(name, password, noteId) 100 | .then(data => { 101 | sendRes(res, {msg: 'SUCCESS'}); 102 | }) 103 | .catch(err => { 104 | sendRes(res, {err}); 105 | }); 106 | }); 107 | 108 | app.get('*', (req, res) => { 109 | res.sendFile(PUBLIC_PATH + '/index.html'); 110 | }); 111 | 112 | app.listen(SERVER_PORT, SERVER_IP, err => { 113 | if(err) 114 | return console.log('An error occured: ', err); 115 | 116 | console.log(`Server listening on ${SERVER_IP}:${SERVER_PORT}`); 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /server/util/mongo.js: -------------------------------------------------------------------------------- 1 | const { MongoClient } = require('mongodb'); 2 | 3 | function connect(url) { 4 | return new Promise((resolve, reject) => { 5 | MongoClient.connect(url, (err, db) => { 6 | if(err) { 7 | reject(err); 8 | return; 9 | } 10 | resolve(db); 11 | }); 12 | }); 13 | }; 14 | 15 | module.exports = { 16 | connect 17 | } 18 | -------------------------------------------------------------------------------- /start-api-server.js: -------------------------------------------------------------------------------- 1 | const pm2 = require('pm2'); 2 | 3 | const scriptName = 'api-server'; 4 | const onError = err => { 5 | if (err) { 6 | console.error(err); 7 | process.exit(2); 8 | } 9 | }; 10 | 11 | pm2.connect(err => { 12 | 13 | 14 | onError(err); 15 | 16 | pm2.list((err, list) => { 17 | 18 | onError(err); 19 | const pList = list.filter(p => p.name === scriptName); 20 | 21 | if(pList.length > 0) { 22 | 23 | if(pList[0].pm2_env.status === 'online') { 24 | 25 | pm2.stop(scriptName, err => { 26 | onError(err); 27 | pm2.start(scriptName, err => { 28 | onError(err); 29 | pm2.disconnect(); 30 | }); 31 | }); 32 | } else { 33 | 34 | pm2.start(scriptName, err => { 35 | onError(err); 36 | pm2.disconnect(); 37 | }); 38 | } 39 | 40 | 41 | 42 | } else { 43 | 44 | pm2.start({ 45 | script: './server/index.js', // Script to be run 46 | name: 'api-server' 47 | }, function(err, apps) { 48 | onError(err); 49 | pm2.disconnect(); // Disconnect from PM2 50 | }); 51 | 52 | } 53 | }); 54 | }) 55 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* No ES6! */ 2 | var path = require('path'), 3 | webpack = require('webpack'), // Da bundling modules! 4 | NpmInstallPlugin = require('npm-install-webpack-plugin'), // Install client dependencies automatically! 5 | merge = require('webpack-merge'), // Merge together configurations! 6 | cssnext = require('postcss-cssnext'), 7 | CONFIG = require('./config/config'); 8 | 9 | const PATHS = { 10 | app: CONFIG.APP_PATH, 11 | build: CONFIG.PUBLIC_PATH, 12 | img: CONFIG.IMG_PATH, 13 | config: CONFIG.CONFIG_PATH 14 | }; 15 | 16 | const TARGET = process.env.npm_lifecycle_event; 17 | 18 | const COMMON_CONFIGURATION = { 19 | entry: { 20 | app: PATHS.app 21 | }, 22 | resolve: { 23 | extensions: ['', '.js', '.jsx'], // Resolve these extensions 24 | }, 25 | output: { 26 | path: PATHS.build, 27 | filename: 'bundle.js' 28 | }, 29 | module: { 30 | loaders: [ 31 | { 32 | test: /\.css$/, 33 | loaders: ['style', 'css', 'postcss'], 34 | include: PATHS.app 35 | }, 36 | { 37 | test: /\.jsx?$/, 38 | loaders: ['babel?cacheDirectory'], 39 | include: PATHS.app 40 | }, 41 | { 42 | test: /\.(jpe?g|png|gif|svg)$/i, 43 | loaders: [ 44 | 'file?hash=sha512&digest=hex&name=[hash].[ext]', 45 | 'image-webpack?bypassOnDebug&optimizationLevel=7&interlaced=false' 46 | ], 47 | include: PATHS.img 48 | }, { 49 | test: /\.worker.js$/, 50 | loaders: ['worker-loader', 'babel?cacheDirectory'], 51 | include: PATHS.app 52 | } 53 | ] 54 | }, 55 | postcss: function() { 56 | return [ cssnext ]; 57 | }, 58 | plugins: [ 59 | new webpack.DefinePlugin({ 60 | ENVIRONMENT: JSON.stringify(TARGET === 'dev' ? 'development' : 'production') 61 | }) 62 | ] 63 | }; 64 | 65 | switch(TARGET) { 66 | // Which procedure was started? 67 | default: 68 | case 'dev': { 69 | module.exports = merge(COMMON_CONFIGURATION, { 70 | devServer: { 71 | contentBase: PATHS.build, 72 | historyApiFallback: true, 73 | hot: true, 74 | inline: true, 75 | progress: true, 76 | stats: 'errors-only' 77 | }, 78 | plugins: [ 79 | new webpack.HotModuleReplacementPlugin(), 80 | new NpmInstallPlugin({ 81 | save: true 82 | }) 83 | ], 84 | devtool: 'eval-source-map' 85 | }); 86 | } 87 | break; 88 | case 'start': { 89 | // Make later when going into production step! 90 | module.exports = merge(COMMON_CONFIGURATION, { 91 | plugins: [ 92 | new webpack.DefinePlugin({ 93 | 'process.env': { 94 | 'NODE_ENV': JSON.stringify('production') 95 | } 96 | }), 97 | new webpack.optimize.UglifyJsPlugin({ 98 | compress: { warnings: false } 99 | }), 100 | new webpack.optimize.DedupePlugin() 101 | ] 102 | }); 103 | } 104 | } 105 | --------------------------------------------------------------------------------