├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── .htaccess ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html └── manifest.json └── src ├── App.js ├── assets └── images │ └── logo.svg ├── components ├── Menu.jsx ├── RawSegment.jsx └── notes │ ├── Attributes.jsx │ ├── Editor.jsx │ ├── Finder.jsx │ ├── Viewer.jsx │ └── editors │ ├── MarkdownEditor.jsx │ └── TextEditor.jsx ├── helpers ├── Api.js ├── Help.js ├── History.js └── MarkdownConverter.js ├── index.js ├── models ├── Notes.js └── index.js ├── pages ├── Home.jsx ├── Notes.jsx ├── Settings.jsx ├── auth │ ├── Login.jsx │ ├── Logout.jsx │ └── Register.jsx └── notes │ └── Editor.jsx ├── stores ├── AppLoading.js ├── Auth.js ├── Editor.js ├── Help.js ├── Settings.js ├── Viewer.js └── index.js └── styles ├── main.css └── scss ├── _semantic.scss ├── components └── _menu.scss ├── main.scss └── pages └── _notes.scss /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | 3 | # Created by https://www.gitignore.io/api/node,sass,macos,windows,intellij 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/ 11 | 12 | # CMake 13 | cmake-build-*/ 14 | 15 | # Mongo Explorer plugin 16 | .idea/**/mongoSettings.xml 17 | 18 | # File-based project format 19 | *.iws 20 | 21 | # IntelliJ 22 | out/ 23 | 24 | # mpeltonen/sbt-idea plugin 25 | .idea_modules/ 26 | 27 | # JIRA plugin 28 | atlassian-ide-plugin.xml 29 | 30 | # Cursive Clojure plugin 31 | .idea/replstate.xml 32 | 33 | # Crashlytics plugin (for Android Studio and IntelliJ) 34 | com_crashlytics_export_strings.xml 35 | crashlytics.properties 36 | crashlytics-build.properties 37 | fabric.properties 38 | 39 | # Editor-based Rest Client 40 | .idea/httpRequests 41 | 42 | ### Intellij Patch ### 43 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 44 | 45 | # *.iml 46 | # modules.xml 47 | # .idea/misc.xml 48 | # *.ipr 49 | 50 | # Sonarlint plugin 51 | .idea/sonarlint 52 | 53 | ### macOS ### 54 | # General 55 | .DS_Store 56 | .AppleDouble 57 | .LSOverride 58 | 59 | # Icon must end with two \r 60 | Icon 61 | 62 | # Thumbnails 63 | ._* 64 | 65 | # Files that might appear in the root of a volume 66 | .DocumentRevisions-V100 67 | .fseventsd 68 | .Spotlight-V100 69 | .TemporaryItems 70 | .Trashes 71 | .VolumeIcon.icns 72 | .com.apple.timemachine.donotpresent 73 | 74 | # Directories potentially created on remote AFP share 75 | .AppleDB 76 | .AppleDesktop 77 | Network Trash Folder 78 | Temporary Items 79 | .apdisk 80 | 81 | ### Node ### 82 | # Logs 83 | logs 84 | *.log 85 | npm-debug.log* 86 | yarn-debug.log* 87 | yarn-error.log* 88 | 89 | # Runtime data 90 | pids 91 | *.pid 92 | *.seed 93 | *.pid.lock 94 | 95 | # Directory for instrumented libs generated by jscoverage/JSCover 96 | lib-cov 97 | 98 | # Coverage directory used by tools like istanbul 99 | coverage 100 | 101 | # nyc test coverage 102 | .nyc_output 103 | 104 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 105 | .grunt 106 | 107 | # Bower dependency directory (https://bower.io/) 108 | bower_components 109 | 110 | # node-waf configuration 111 | .lock-wscript 112 | 113 | # Compiled binary addons (https://nodejs.org/api/addons.html) 114 | build/Release 115 | 116 | # Dependency directories 117 | node_modules/ 118 | jspm_packages/ 119 | 120 | # TypeScript v1 declaration files 121 | typings/ 122 | 123 | # Optional npm cache directory 124 | .npm 125 | 126 | # Optional eslint cache 127 | .eslintcache 128 | 129 | # Optional REPL history 130 | .node_repl_history 131 | 132 | # Output of 'npm pack' 133 | *.tgz 134 | 135 | # Yarn Integrity file 136 | .yarn-integrity 137 | 138 | # dotenv environment variables file 139 | .env 140 | 141 | # parcel-bundler cache (https://parceljs.org/) 142 | .cache 143 | 144 | # next.js build output 145 | .next 146 | 147 | # nuxt.js build output 148 | .nuxt 149 | 150 | # vuepress build output 151 | .vuepress/dist 152 | 153 | # Serverless directories 154 | .serverless 155 | 156 | ### Sass ### 157 | .sass-cache/ 158 | *.css.map 159 | *.sass.map 160 | *.scss.map 161 | 162 | ### Windows ### 163 | # Windows thumbnail cache files 164 | Thumbs.db 165 | ehthumbs.db 166 | ehthumbs_vista.db 167 | 168 | # Dump file 169 | *.stackdump 170 | 171 | # Folder config file 172 | [Dd]esktop.ini 173 | 174 | # Recycle Bin used on file shares 175 | $RECYCLE.BIN/ 176 | 177 | # Windows Installer files 178 | *.cab 179 | *.msi 180 | *.msix 181 | *.msm 182 | *.msp 183 | 184 | # Windows shortcuts 185 | *.lnk 186 | 187 | 188 | # End of https://www.gitignore.io/api/node,sass,macos,windows,intellij -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Calepin 3 |

4 | 5 | Calepin is a small open source self-hostable note-taking web app. 6 | 7 | It is based on two packages: 8 | 9 | - The API, [calepin-api](https://github.com/orditeck/calepin-api). RESTful PHP API using Laravel 5.6. 10 | - The web app, [calepin-frontend](https://github.com/orditeck/calepin-frontend). Using React. 11 | 12 | __Please note:__ A complete rewrite of Calepin, both frontend & backend, is planned in mid-2020. Star & Watch the repo to get updated on the progress! 13 | 14 | ## Use it 15 | 16 | There are several ways you can use Calepin. 17 | 18 | 1. Use [calepin.io](https://app.calepin.io/) services for free. You can encrypt your notes in AES-256 client-side, so the server will never know what's inside your notes. 19 | 2. Use [calepin.io](https://app.calepin.io/) with [your self-hosted API](https://github.com/orditeck/calepin-api). You can set the API URL in the settings. 20 | 3. Hosting everything, see below. 21 | 22 | ## Host it 23 | 24 | Hosting the front-end is quite simple, it's only HTML/CSS/JS, so you can host it wherever you'd like. 25 | 26 | 1. `git clone git@github.com:orditeck/calepin-frontend.git` 27 | 2. `cd calepin-frontend` 28 | 3. `npm install` 29 | 4. `npm run build` 30 | 5. Upload the content of the `build` folder to your web server 31 | 32 | You can then either use the Calepin public API (at https://api.calepin.io/api/v1) or [install the API on your server](https://github.com/orditeck/calepin-api). 33 | 34 | By default, when you open your self-hosted Calepin's frontend, it'll use the default API, `api.calepin.io`. You can change the API URL in the settings and it'll be remembered on your device as long as don't clear the app storage, but you'd have to do this on every devices you use and when sharing a note to someone that never set your API URL in the settings, it'd try to fetch the note from `api.calepin.io` (feature to be added later). And anyway, at this point, I expect you'll want to use your own API all the time. 35 | 36 | ### Set your API as the default one 37 | 38 | 1. Go to https://github.com/orditeck/calepin-frontend/blob/master/src/stores/Settings.js#L5 39 | 2. Replace the URL with yours 40 | 3. Build and enjoy! 41 | 42 | ## Contribute to it 43 | 44 | 1. `git clone git@github.com:orditeck/calepin-frontend.git` 45 | 2. `npm install` 46 | 3. `npm run start` 47 | 48 | ## To-dos 49 | 50 | Non-exhaustive and unordered 51 | - [ ] Button to clear local/session storage after logout 52 | - [ ] Tags/folders + folder sharing with navigation + search 53 | - [ ] Some kind of pagination on notes listing 54 | - [ ] Autosave 55 | - [ ] Notes versionning/history 56 | - [ ] Mobile friendly 57 | - [ ] Mobile app 58 | - [ ] A website 59 | - [ ] UI refactor 60 | - [ ] Export notes 61 | - [ ] Import notes 62 | - [ ] Handle custom server when sharing public note 63 | 64 | ## License 65 | 66 | Copyright (C) 2018 Keven "orditeck" Lefebvre 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calepin-frontend", 3 | "version": "1.0.0-beta1", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.18.0", 7 | "crypto-js": "^3.1.9-1", 8 | "deepmerge": "^2.1.1", 9 | "draft-js": "^0.10.5", 10 | "history": "^4.7.2", 11 | "react": "^16.4.1", 12 | "react-app-state": "^0.0.4", 13 | "react-dom": "^16.4.1", 14 | "react-mde": "^5.7.0", 15 | "react-router-dom": "^4.3.1", 16 | "react-scripts": "1.1.4", 17 | "react-syntax-highlighter": "^7.0.4", 18 | "semantic-ui-css": "^2.3.2", 19 | "semantic-ui-react": "^0.81.2", 20 | "showdown": "^1.8.6" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test --env=jsdom", 26 | "eject": "react-scripts eject" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteCond %{REQUEST_FILENAME} -s [OR] 3 | RewriteCond %{REQUEST_FILENAME} -l [OR] 4 | RewriteCond %{REQUEST_FILENAME} -d 5 | RewriteRule ^.*$ - [NC,L] 6 | 7 | RewriteRule ^(.*) /index.html [NC,L] -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orditeck/calepin-frontend/b0e5e109f8848f161853e01c6a0807954146b0e1/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orditeck/calepin-frontend/b0e5e109f8848f161853e01c6a0807954146b0e1/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orditeck/calepin-frontend/b0e5e109f8848f161853e01c6a0807954146b0e1/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orditeck/calepin-frontend/b0e5e109f8848f161853e01c6a0807954146b0e1/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Calepin 13 | 14 | 15 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Calepin", 3 | "name": "Calepin", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#4a4799", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import AppLoading from './stores/AppLoading'; 4 | import AuthStore from './stores/Auth'; 5 | 6 | import Menu from './components/Menu'; 7 | import RawSegment from './components/RawSegment'; 8 | 9 | export default AuthStore.subscribe( 10 | AppLoading.subscribe( 11 | class App extends Component { 12 | render() { 13 | return ( 14 | 15 |
16 | 17 |
18 |
19 | 20 | {this.props.children} 21 | 22 |
23 |
24 | ); 25 | } 26 | } 27 | ) 28 | ); 29 | -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | logo -------------------------------------------------------------------------------- /src/components/Menu.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Menu, Icon, Popup } from 'semantic-ui-react'; 4 | 5 | import AuthStore from '../stores/Auth'; 6 | import logo from '../assets/images/logo.svg'; 7 | 8 | export default AuthStore.subscribe( 9 | class App extends Component { 10 | renderIconItem = (title, icon, url) => ( 11 | 14 | 15 | 16 | } 17 | content={title} 18 | inverted 19 | position="right center" 20 | /> 21 | ); 22 | 23 | render() { 24 | return ( 25 | 26 | 27 | Calepin 28 | 29 | {!this.props.logged_in ? ( 30 | 31 | 32 | 33 | 34 | ) : ( 35 | 36 | {this.renderIconItem('Your notes', 'sticky note', '/notes')} 37 | {this.renderIconItem('New note', 'add circle', '/notes/new')} 38 | {this.renderIconItem('Settings', 'settings', '/settings')} 39 | {this.renderIconItem('Logout', 'log out', '/auth/logout')} 40 | 41 | )} 42 | 43 | ); 44 | } 45 | } 46 | ); 47 | -------------------------------------------------------------------------------- /src/components/RawSegment.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classnames from 'classnames'; 3 | import { Segment } from 'semantic-ui-react'; 4 | 5 | export default class extends Component { 6 | render() { 7 | return ( 8 | 9 | {this.props.children} 10 | 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/notes/Attributes.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Menu, Popup, Icon, Form, Select, Checkbox, Confirm } from 'semantic-ui-react'; 3 | import { AuthStore, EditorStore, SettingsStore } from '../../stores'; 4 | import { Notes } from '../../models'; 5 | import History from '../../helpers/History'; 6 | import RawSegment from '../RawSegment'; 7 | 8 | export default EditorStore.subscribe( 9 | SettingsStore.subscribe( 10 | class extends Component { 11 | state = { 12 | encryptionModalOpen: false 13 | }; 14 | 15 | languages = [ 16 | { key: 'raw', text: 'raw (no highlight)', value: 'raw' }, 17 | 'bash', 18 | 'coffeescript', 19 | 'css', 20 | 'dns', 21 | 'dockerfile', 22 | 'http', 23 | 'javascript', 24 | 'json', 25 | 'makefile', 26 | 'markdown', 27 | 'nginx', 28 | 'php', 29 | 'python', 30 | 'ruby', 31 | 'scss', 32 | 'shell', 33 | 'sql', 34 | 'twig', 35 | 'typescript', 36 | 'vim', 37 | 'xml', 38 | 'yaml' 39 | ].map(e => (typeof e === 'object' ? e : { key: e, text: e, value: e })); 40 | 41 | toggleEditorMode = mode => () => 42 | EditorStore.set({ 43 | mode: mode 44 | }); 45 | 46 | handleSave = () => { 47 | EditorStore.set({ loading: true }); 48 | Notes.save(this.props.note) 49 | .then(({ data }) => { 50 | if (this.props.note.id !== data.data.id) { 51 | History.push(`/notes/edit/${data.data.id}`); 52 | } 53 | 54 | EditorStore.set({ 55 | note: data.data, 56 | originalNote: data.data, 57 | loading: false 58 | }); 59 | }) 60 | .catch(() => { 61 | EditorStore.set({ loading: false }); 62 | }); 63 | }; 64 | 65 | handleClose = () => { 66 | if (JSON.stringify(this.props.note) !== JSON.stringify(this.props.originalNote)) { 67 | if ( 68 | window.confirm( 69 | "There are unsaved changed to your note, if you continue, you'll lose those changes. Do you want to continue?" 70 | ) 71 | ) { 72 | this.discard(); 73 | } 74 | } else { 75 | this.discard(); 76 | } 77 | }; 78 | 79 | discard = () => { 80 | if (this.props.originalNote.id) { 81 | History.push(`/notes/view/${this.props.originalNote.id}`); 82 | } else { 83 | History.push(`/notes`); 84 | } 85 | }; 86 | 87 | onNoteChange = e => 88 | EditorStore.set({ 89 | note: { 90 | ...EditorStore.get('note'), 91 | [e.target.name]: e.target.value 92 | } 93 | }); 94 | 95 | onLanguageChange = (e, { value }) => 96 | EditorStore.set({ 97 | note: { 98 | ...EditorStore.get('note'), 99 | language: value 100 | } 101 | }); 102 | 103 | onMarkdownEditorLayoutChange = (e, { value }) => 104 | SettingsStore.set({ 105 | mdeLayout: value 106 | }); 107 | 108 | onToggleChange = (e, { name, checked }) => { 109 | if (name === 'encrypted' && checked) this.checkEncryption(); 110 | EditorStore.set({ 111 | note: { 112 | ...EditorStore.get('note'), 113 | [name]: checked 114 | } 115 | }); 116 | }; 117 | 118 | checkEncryption = () => 119 | this.setState({ encryptionModalOpen: !AuthStore.get('encryption_key') }); 120 | 121 | renderEncryptionModal = () => ( 122 | { 127 | this.setState({ encryptionModalOpen: false }); 128 | EditorStore.set({ 129 | note: { 130 | ...EditorStore.get('note'), 131 | encrypted: false 132 | } 133 | }); 134 | }} 135 | onConfirm={() => { 136 | History.push('/settings'); 137 | }} 138 | content={ 139 |
140 | You must specify your encryption key before you can turn on encryption 141 | on your note. Would you like to set one now?
142 |
143 | Your current changes will be lost. 144 |
145 | } 146 | /> 147 | ); 148 | 149 | render() { 150 | return ( 151 | 152 | 153 | 154 | Save 155 | 156 | 157 | Close 158 | 159 | 165 | 166 | 167 | } 168 | content="Markdown editor" 169 | inverted 170 | /> 171 | 177 | 178 | 179 | } 180 | content="Text editor" 181 | inverted 182 | /> 183 | 184 | 185 |
186 | 194 | 203 | {this.props.mode === 'markdown' ? ( 204 | 223 | ) : ( 224 | '' 225 | )} 226 | 234 | 242 | 243 | {this.renderEncryptionModal()} 244 | 245 |
246 | ); 247 | } 248 | } 249 | ) 250 | ); 251 | -------------------------------------------------------------------------------- /src/components/notes/Editor.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Segment, Confirm } from 'semantic-ui-react'; 3 | 4 | import MarkdownEditor from './editors/MarkdownEditor'; 5 | import TextEditor from './editors/TextEditor'; 6 | import { EditorStore, HelpStore } from '../../stores'; 7 | import { Notes } from '../../models'; 8 | import History from '../../helpers/History'; 9 | import { checkIfShouldRenderFirstNoteEncryptionNotice } from '../../helpers/Help'; 10 | 11 | export default HelpStore.subscribe( 12 | EditorStore.subscribe( 13 | class extends Component { 14 | componentWillMount = () => { 15 | checkIfShouldRenderFirstNoteEncryptionNotice(); 16 | if (this.props.match.params.id) { 17 | EditorStore.set({ 18 | type: 'edit', 19 | loading: true 20 | }); 21 | Notes.find(this.props.match.params.id).then(({ data }) => 22 | EditorStore.set({ 23 | loading: false, 24 | note: data.data, 25 | originalNote: data.data, 26 | mdeState: { 27 | markdown: data.data.content 28 | } 29 | }) 30 | ); 31 | } else { 32 | EditorStore.new(); 33 | } 34 | }; 35 | 36 | componentWillUnmount = () => EditorStore.reset(); 37 | 38 | renderFirstNoteAlert = () => ( 39 | { 44 | HelpStore.set({ renderFirstNoteAlert: 'do-not-ask-again' }); 45 | }} 46 | onConfirm={() => { 47 | HelpStore.set({ renderFirstNoteAlert: 'do-not-ask-again' }); 48 | History.push('/settings'); 49 | }} 50 | content={ 51 |
52 |

You're adding your first note.

53 | 54 |

A word about encryption.

55 | 56 |

57 | Encryption is done 100% client-side, meaning the server never sees 58 | the content of the non-encrypted note. The title is not encrypted, 59 | so be sure to use a non-confidential title for your notes. 60 |

61 | 62 |

You can choose to encrypt on a per-note basis.

63 | 64 |

65 | You must provide an encryption key to encrypt your notes. The same 66 | encryption key will be needed every time you want to read the note. 67 | If you lose your key, you lose your note(s) that are using that key. 68 |

69 | 70 |

71 | You can use a different encryption key for a group of notes (or even 72 | every note), but you'll have to go back and forth to the settings 73 | page every time. 74 |

75 | 76 |

Would you like to set your encryption key now?

77 |
78 | } 79 | /> 80 | ); 81 | 82 | render() { 83 | return ( 84 |
85 | {this.renderFirstNoteAlert()} 86 | 87 | 88 | {this.props.mode === 'text' ? : } 89 | 90 |
91 | ); 92 | } 93 | } 94 | ) 95 | ); 96 | -------------------------------------------------------------------------------- /src/components/notes/Finder.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Header, Menu } from 'semantic-ui-react'; 3 | import { ViewerStore } from '../../stores'; 4 | import { Notes } from '../../models'; 5 | import RawSegment from '../RawSegment'; 6 | 7 | export default ViewerStore.subscribe( 8 | class extends Component { 9 | state = { 10 | loading: false 11 | }; 12 | 13 | componentDidUpdate = prevProps => { 14 | if (prevProps.location.pathname !== this.props.location.pathname) { 15 | this.refreshNotes(); 16 | } 17 | }; 18 | 19 | componentDidMount = () => this.refreshNotes(); 20 | 21 | refreshNotes = () => { 22 | this.setState({ loading: true }); 23 | Notes.get().then(({ data }) => { 24 | this.setState({ loading: false }); 25 | ViewerStore.set({ notes: data.data }); 26 | }); 27 | }; 28 | 29 | openNote = note => () => this.props.history.push(`/notes/view/${note.id}`); 30 | 31 | renderNotes = () => { 32 | const { notes } = this.props; 33 | 34 | if (notes.length === 0) { 35 | return ( 36 | this.props.history.push('/notes/new')}> 37 |

Your notes will appear here. Click here to start writing one!

38 |
39 | ); 40 | } else { 41 | return notes.map(note => { 42 | const shortContent = note.encrypted 43 | ? 'Encrypted note' 44 | : note.content.substr(0, 100) + (note.content.length > 100 ? '\u2026' : ''); 45 | return ( 46 | 47 |
{note.title}
48 |

{shortContent}

49 |
50 | ); 51 | }); 52 | } 53 | }; 54 | 55 | render() { 56 | return ( 57 | 58 | {this.renderNotes()} 59 | 60 | ); 61 | } 62 | } 63 | ); 64 | -------------------------------------------------------------------------------- /src/components/notes/Viewer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Segment, Menu, Icon, Message, Modal, Button } from 'semantic-ui-react'; 3 | import SyntaxHighlighter from 'react-syntax-highlighter'; 4 | import { docco } from 'react-syntax-highlighter/styles/hljs'; 5 | import CryptoJS from 'crypto-js'; 6 | 7 | import { AuthStore, ViewerStore } from '../../stores'; 8 | import { Notes } from '../../models'; 9 | import MarkdownConverter from '../../helpers/MarkdownConverter'; 10 | import History from '../../helpers/History'; 11 | 12 | export default ViewerStore.subscribe( 13 | class extends Component { 14 | state = { 15 | renderDeleteConfirm: false 16 | }; 17 | 18 | componentWillMount = () => { 19 | if (this.props.match.params.id) { 20 | ViewerStore.set({ loading: true }); 21 | Notes.find(this.props.match.params.id).then(({ data }) => 22 | ViewerStore.set({ 23 | loading: false, 24 | note: data.data 25 | }) 26 | ); 27 | } 28 | }; 29 | 30 | componentWillUnmount = () => 31 | ViewerStore.set({ 32 | note: null 33 | }); 34 | 35 | handleEdit = () => History.push(`/notes/edit/${this.props.note.id}`); 36 | 37 | handleDelete = () => this.setState({ renderDeleteConfirm: true }); 38 | 39 | handleClose = () => History.push('/notes'); 40 | 41 | renderDeleteConfirmation = () => ( 42 | this.setState({ renderDeleteConfirm: false })} 45 | > 46 | Delete a note 47 | 48 |

Are you sure you want to delete this note? This action is not reversible.

49 |
50 | 51 | 54 | {' '} 33 | or when logging out. 34 | 35 | } 36 | /> 37 | 38 |
39 | 49 | 55 | API URL. Always use HTTPS. 56 | 57 | } 58 | value={this.props.api_url} 59 | onChange={this.onChange} 60 | /> 61 | 62 | 63 | 64 | ); 65 | } 66 | } 67 | ); 68 | -------------------------------------------------------------------------------- /src/pages/auth/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Form, Segment } from 'semantic-ui-react'; 3 | 4 | import Auth from '../../stores/Auth'; 5 | import Api from '../../helpers/Api'; 6 | 7 | export default Auth.subscribe( 8 | class extends Component { 9 | state = { email: '', password: '' }; 10 | 11 | handleChange = (e, { name, value }) => this.setState({ [name]: value }); 12 | 13 | handleSubmit = () => { 14 | Api.post(`auth/login`, this.state).then(({ status, data }) => { 15 | Auth.set({ 16 | logged_in: true, 17 | user: data.data, 18 | access_token: data.meta.access_token 19 | }); 20 | this.redirectIfLoggedIn(); 21 | }); 22 | }; 23 | 24 | componentWillMount = () => this.redirectIfLoggedIn(); 25 | 26 | redirectIfLoggedIn() { 27 | if (this.props.logged_in) { 28 | this.props.history.push('/notes'); 29 | } 30 | } 31 | 32 | render() { 33 | const { email, password } = this.state; 34 | 35 | return ( 36 | 37 |

Login

38 |
39 | 47 | 55 | 56 | 57 |
58 | ); 59 | } 60 | } 61 | ); 62 | -------------------------------------------------------------------------------- /src/pages/auth/Logout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Segment, Button } from 'semantic-ui-react'; 3 | import AuthStore, { DefaultState as DefaultAuthState } from '../../stores/Auth'; 4 | import SettingsStore, { DefaultState as DefaultSettingsState } from '../../stores/Settings'; 5 | import EditorStore, { DefaultState as DefaultEditorState } from '../../stores/Editor'; 6 | 7 | export default AuthStore.subscribe( 8 | class extends Component { 9 | state = { 10 | cleared: false 11 | }; 12 | 13 | componentWillMount() { 14 | AuthStore.set(DefaultAuthState); 15 | } 16 | 17 | clearCache = () => { 18 | SettingsStore.set(DefaultSettingsState); 19 | EditorStore.set(DefaultEditorState); 20 | localStorage.clear(); 21 | sessionStorage.clear(); 22 | this.setState({ 23 | cleared: true 24 | }); 25 | }; 26 | 27 | render() { 28 | return ( 29 | 30 |

You are now logged out.

31 | 32 |

Would you like to clear all the application saved settings?

33 | 34 |

It's better if you're using a shared computer in a non-incognito mode.

35 | 36 | {!this.state.cleared ? ( 37 | 38 | ) : ( 39 |

All caches have been cleared.

40 | )} 41 |
42 | ); 43 | } 44 | } 45 | ); 46 | -------------------------------------------------------------------------------- /src/pages/auth/Register.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Form, Segment } from 'semantic-ui-react'; 3 | 4 | import Auth from '../../stores/Auth'; 5 | import Api from '../../helpers/Api'; 6 | 7 | export default Auth.subscribe( 8 | class extends Component { 9 | state = { first_name: '', last_name: '', email: '', password: '' }; 10 | 11 | handleChange = (e, { name, value }) => this.setState({ [name]: value }); 12 | 13 | handleSubmit = () => { 14 | Api.post(`auth/register`, this.state).then(({ status, data }) => { 15 | Auth.set({ 16 | logged_in: true, 17 | user: data.data, 18 | access_token: data.meta.access_token 19 | }); 20 | this.redirectIfLoggedIn(); 21 | }); 22 | }; 23 | 24 | componentWillMount = () => this.redirectIfLoggedIn(); 25 | 26 | redirectIfLoggedIn() { 27 | if (this.props.logged_in) { 28 | this.props.history.push('/notes'); 29 | } 30 | } 31 | 32 | render() { 33 | const { first_name, last_name, email, password } = this.state; 34 | 35 | return ( 36 | 37 |

Register

38 |
39 | 47 | 55 | 63 | 71 | 72 | 73 |
74 | ); 75 | } 76 | } 77 | ); 78 | -------------------------------------------------------------------------------- /src/pages/notes/Editor.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Grid, Segment } from 'semantic-ui-react'; 3 | 4 | import NoteStore, { EmptyNote } from '../../stores/Note'; 5 | import Editor from '../../components/notes/Editor'; 6 | import Attributes from '../../components/notes/Attributes'; 7 | 8 | export default NoteStore.subscribe( 9 | class extends Component { 10 | componentWillMount = () => { 11 | if (!this.props.editor.note) { 12 | NoteStore.mergeSet({ 13 | editor: { 14 | originalNote: EmptyNote, 15 | note: EmptyNote 16 | } 17 | }); 18 | } 19 | }; 20 | 21 | render() { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /src/stores/AppLoading.js: -------------------------------------------------------------------------------- 1 | import AppState from 'react-app-state'; 2 | 3 | export default new AppState({ 4 | loading: false 5 | }); 6 | -------------------------------------------------------------------------------- /src/stores/Auth.js: -------------------------------------------------------------------------------- 1 | import AppState from 'react-app-state'; 2 | 3 | export let DefaultState = { 4 | logged_in: false, 5 | access_token: null, 6 | encryption_key: '', 7 | 8 | user: { 9 | id: null, 10 | email: null, 11 | first_name: null, 12 | last_name: null, 13 | previous_login: null 14 | } 15 | }; 16 | 17 | export default new class extends AppState { 18 | constructor() { 19 | super(JSON.parse(sessionStorage.getItem('AuthStore')) || DefaultState); 20 | } 21 | 22 | set(data) { 23 | super.set(data); 24 | sessionStorage.setItem('AuthStore', JSON.stringify(this._propsAndValues)); 25 | } 26 | }(); 27 | -------------------------------------------------------------------------------- /src/stores/Editor.js: -------------------------------------------------------------------------------- 1 | import AppState from 'react-app-state'; 2 | import { AuthStore } from './'; 3 | 4 | export const EmptyNote = { 5 | id: null, 6 | title: '', 7 | content: '', 8 | author_id: null, 9 | language: null, 10 | encrypted: false, 11 | public: false 12 | }; 13 | 14 | export const DefaultState = { 15 | type: 'new', 16 | mode: 'markdown', 17 | loading: false, 18 | mdeState: { 19 | markdown: '' 20 | }, 21 | originalNote: EmptyNote, 22 | note: EmptyNote 23 | }; 24 | 25 | export default new class extends AppState { 26 | constructor() { 27 | super(DefaultState); 28 | } 29 | 30 | new = () => 31 | this.set({ 32 | type: 'new', 33 | originalNote: { 34 | ...EmptyNote, 35 | author_id: AuthStore.get('user').id 36 | }, 37 | note: { 38 | ...EmptyNote, 39 | author_id: AuthStore.get('user').id 40 | } 41 | }); 42 | 43 | reset = () => this.set(DefaultState); 44 | }(); 45 | -------------------------------------------------------------------------------- /src/stores/Help.js: -------------------------------------------------------------------------------- 1 | import AppState from 'react-app-state'; 2 | 3 | export default new AppState({ 4 | renderFirstNoteAlert: false 5 | }); 6 | -------------------------------------------------------------------------------- /src/stores/Settings.js: -------------------------------------------------------------------------------- 1 | import AppState from 'react-app-state'; 2 | 3 | export let DefaultState = { 4 | encryption_key: '', 5 | api_url: 'https://api.calepin.io/api/v1', 6 | mdeLayout: 'tabbed' 7 | }; 8 | 9 | export default new class extends AppState { 10 | constructor() { 11 | super(JSON.parse(sessionStorage.getItem('SettingsStore')) || DefaultState); 12 | } 13 | 14 | set(data) { 15 | super.set(data); 16 | sessionStorage.setItem('SettingsStore', JSON.stringify(this._propsAndValues)); 17 | } 18 | }(); 19 | -------------------------------------------------------------------------------- /src/stores/Viewer.js: -------------------------------------------------------------------------------- 1 | import AppState from 'react-app-state'; 2 | 3 | export default new class extends AppState { 4 | constructor() { 5 | super({ 6 | loading: false, 7 | note: null, 8 | notes: [] 9 | }); 10 | } 11 | }(); 12 | -------------------------------------------------------------------------------- /src/stores/index.js: -------------------------------------------------------------------------------- 1 | export { default as AuthStore } from './Auth'; 2 | export { default as ViewerStore } from './Viewer'; 3 | export { default as EditorStore } from './Editor'; 4 | export { default as HelpStore } from './Help'; 5 | export { default as SettingsStore } from './Settings'; 6 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | .ui.segment.raw { 2 | padding: 0; } 3 | 4 | .half-page { 5 | width: 50vw; } 6 | 7 | .app-container { 8 | display: flex; } 9 | .app-container .menu-container { 10 | flex: 1; 11 | height: 100vh; 12 | background: #e7e7e8; 13 | width: 80px; 14 | max-width: 80px; } 15 | .app-container .menu-container .vertical.menu { 16 | width: 100%; 17 | background: transparent; 18 | box-shadow: none; 19 | border: none; } 20 | .app-container .menu-container .vertical.menu .item { 21 | text-align: center; 22 | padding: 14px 0; } 23 | .app-container .menu-container .vertical.menu .item:before { 24 | display: none; } 25 | .app-container .menu-container .vertical.menu .item:first-of-type { 26 | margin-bottom: 20px; } 27 | .app-container .menu-container .vertical.menu .item i { 28 | font-size: 24px; 29 | color: #5352a0; } 30 | .app-container .menu-container .vertical.menu .item > img:not(.ui) { 31 | max-width: 70%; 32 | margin: 0 auto; } 33 | .app-container .page-container { 34 | flex: 15; } 35 | 36 | .page-notes { 37 | display: flex; } 38 | .page-notes > .sidebar { 39 | flex: 1; } 40 | .page-notes > .sidebar > .segment { 41 | height: 100vh; 42 | overflow-y: scroll; 43 | overflow-x: hidden; } 44 | .page-notes > .sidebar > .segment > .menu { 45 | width: 100%; 46 | border-width: 0; 47 | border-radius: 0; } 48 | .page-notes > .sidebar > .segment.props-editor > .menu { 49 | border-bottom-width: 1px; 50 | padding: 3px 0; } 51 | .page-notes > .main { 52 | flex: 3; } 53 | .page-notes > .main .calepin-editor, 54 | .page-notes > .main .calepin-viewer { 55 | height: calc(100vh); 56 | overflow-y: scroll; 57 | overflow-x: hidden; } 58 | .page-notes > .main .calepin-editor > .ui.bottom.attached.segment { 59 | border: 0; 60 | padding: 0; } 61 | .page-notes > .main .calepin-editor > .ui.bottom.attached.segment .react-mde, .page-notes > .main .calepin-editor > .ui.bottom.attached.segment .mde-header, .page-notes > .main .calepin-editor > .ui.bottom.attached.segment .react-mde-vertical-layout .react-mde-content .mde-preview { 62 | border-color: rgba(34, 36, 38, 0.15); } 63 | .page-notes > .main .calepin-editor > .ui.bottom.attached.segment .react-mde { 64 | border: 0; } 65 | .page-notes > .main .calepin-viewer { 66 | height: 100%; } 67 | .page-notes > .main .calepin-viewer > .segment { 68 | height: 100vh; 69 | border: 0; 70 | box-shadow: none; } 71 | .page-notes > .main .calepin-viewer > .top.menu { 72 | border-top: none; 73 | border-left: none; 74 | border-radius: 0; } 75 | .page-notes > .main .calepin-viewer > .bottom.attached.segment { 76 | height: calc(100vh - 40px); 77 | white-space: pre-wrap; } 78 | .page-notes > .main .calepin-viewer > .bottom.attached.segment .mde-preview { 79 | white-space: normal; } 80 | 81 | /*# sourceMappingURL=main.css.map */ 82 | -------------------------------------------------------------------------------- /src/styles/scss/_semantic.scss: -------------------------------------------------------------------------------- 1 | .ui.segment.raw { 2 | padding: 0; 3 | } 4 | 5 | .half-page { 6 | width: 50vw; 7 | } -------------------------------------------------------------------------------- /src/styles/scss/components/_menu.scss: -------------------------------------------------------------------------------- 1 | .vertical.menu { 2 | width: 100%; 3 | background: transparent; 4 | box-shadow: none; 5 | border: none; 6 | 7 | .item { 8 | text-align: center; 9 | padding: 14px 0; 10 | 11 | &:before { 12 | display: none; 13 | } 14 | 15 | &:first-of-type { 16 | margin-bottom: 20px; 17 | } 18 | 19 | i { 20 | font-size: 24px; 21 | color: #5352a0; 22 | } 23 | 24 | > img:not(.ui) { 25 | max-width: 70%; 26 | margin: 0 auto; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/styles/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import "semantic"; 2 | 3 | .app-container { 4 | display: flex; 5 | 6 | .menu-container { 7 | flex: 1; 8 | height: 100vh; 9 | background: #e7e7e8; 10 | width: 80px; 11 | max-width: 80px; 12 | @import "components/menu"; 13 | } 14 | 15 | .page-container { 16 | flex: 15; 17 | } 18 | } 19 | 20 | @import "pages/notes"; -------------------------------------------------------------------------------- /src/styles/scss/pages/_notes.scss: -------------------------------------------------------------------------------- 1 | .page-notes { 2 | display: flex; 3 | 4 | > .sidebar { 5 | flex: 1; 6 | 7 | > .segment { 8 | height: 100vh; 9 | overflow-y: scroll; 10 | overflow-x: hidden; 11 | 12 | > .menu { 13 | width: 100%; 14 | border-width: 0; 15 | border-radius: 0; 16 | } 17 | 18 | &.props-editor { 19 | > .menu { 20 | border-bottom-width: 1px; 21 | padding: 3px 0; 22 | } 23 | } 24 | } 25 | } 26 | 27 | > .main { 28 | flex: 3; 29 | 30 | .calepin-editor, 31 | .calepin-viewer { 32 | height: calc(100vh); 33 | overflow-y: scroll; 34 | overflow-x: hidden; 35 | } 36 | 37 | .calepin-editor { 38 | > .ui.bottom.attached.segment { 39 | .react-mde, .mde-header, .react-mde-vertical-layout .react-mde-content .mde-preview { 40 | border-color: rgba(34,36,38,.15); 41 | } 42 | 43 | .react-mde { 44 | border: 0; 45 | } 46 | 47 | border: 0; 48 | padding: 0; 49 | } 50 | } 51 | 52 | .calepin-viewer { 53 | height: 100%; 54 | 55 | > .segment { 56 | height: 100vh; 57 | border: 0; 58 | box-shadow: none; 59 | } 60 | 61 | > .top.menu { 62 | border-top: none; 63 | border-left: none; 64 | border-radius: 0; 65 | } 66 | 67 | > .bottom.attached.segment { 68 | height: calc(100vh - 40px); 69 | white-space: pre-wrap; 70 | 71 | .mde-preview { 72 | white-space: normal; 73 | } 74 | } 75 | 76 | } 77 | } 78 | 79 | } --------------------------------------------------------------------------------