├── .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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------