├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── examples │ ├── dm.png │ ├── gnk1.png │ ├── gnk2.png │ ├── goals1.png │ ├── goals2.png │ ├── goals3.png │ ├── interface.png │ ├── mast1.png │ ├── mast2.png │ ├── nulis-screenshot1.png │ ├── nulis-screenshot2.png │ ├── pricing.png │ ├── pricing1.png │ ├── pricing2.png │ ├── pricing3.png │ ├── pricing4.png │ ├── pricing5.png │ ├── scrum1.pdf │ ├── scrum2.png │ ├── scrum3.png │ ├── scrum4.png │ ├── scrum5.png │ ├── scrum6.png │ ├── template1.png │ ├── template2.png │ ├── text.jpg │ ├── text2.jpg │ ├── text3.png │ ├── text4.png │ ├── true-novelist_796294_full.jpg │ └── tumblr_inline_mpogqunvFj1qz4rgp.png ├── history-v0.1.png ├── logo_128x128.png ├── logo_full.png ├── logo_giant.png ├── logo_text.png ├── rsz_screenshot-10-stats2.png ├── rsz_screenshot-11-leaderboard.png ├── screenshot-1.png ├── screenshot-10-stats2.png ├── screenshot-11-story-template.png ├── screenshot-2.png ├── screenshot-3.png ├── screenshot-4-search.png ├── screenshot-5-mobile.png ├── screenshot-6-checkboxes-and-colors.png ├── screenshot-7-tags.png ├── screenshot-8-stats.png ├── screenshot-9-prompt.png ├── social-leaderboard.png ├── social-prompts.png ├── social.png └── trees │ ├── About (4).nls │ ├── About (5).nls │ ├── about.nls │ ├── about_back1.nls │ ├── about_back2.nls │ ├── blank.nls │ ├── business.nls │ ├── community.nls │ ├── large.nls │ ├── nulis.nls │ ├── nulis2.nls │ ├── onecolumn.nls │ ├── prompt.nls │ ├── sharks.nls │ ├── story.nls │ ├── story_back.nls │ ├── test.nls │ ├── test2.nls │ ├── test3.nls │ ├── writing-prompt.nls │ ├── writing-prompt2.nls │ └── writingstreak.nls ├── client ├── #index.html# ├── #webpack.config.js# ├── .babelrc ├── actions │ ├── cards.actions.js │ ├── preferences.actions.js │ ├── profiles.actions.js │ └── trees.actions.js ├── components │ ├── #App.js# │ ├── #Card.js# │ ├── #ModalThankYou.js# │ ├── #Search.js# │ ├── #Stats.js# │ ├── .#ModalThankYou.js │ ├── App.js │ ├── Card.js │ ├── CardGroup.js │ ├── CardLimit.js │ ├── ColorBox.js │ ├── ComponentBoilerplate.js │ ├── DebuggingPanel.js │ ├── DropTarget.js │ ├── Editor.js │ ├── EditorTextarea.js │ ├── Header.js │ ├── Hotkeys.js │ ├── Main.js │ ├── MenuAbout.js │ ├── MenuEdit.js │ ├── MenuProfile.js │ ├── MenuTree.js │ ├── MetaInfo.js │ ├── ModalDesktop.js │ ├── ModalFree.js │ ├── ModalLogin.js │ ├── ModalPayments.js │ ├── ModalShare.js │ ├── ModalSupport.js │ ├── ModalThankYou.js │ ├── ModalTreeSettings.js │ ├── Search.js │ ├── Stats.js │ ├── TreeManager.js │ └── auth │ │ ├── login.js │ │ ├── require_auth.js │ │ ├── signin.js │ │ ├── signout.js │ │ └── signup.js ├── data.js ├── dist │ ├── 674f50d287a8c48dc19ba404d20fe713.eot │ ├── 89889688147bd7575d6327160d64e760.svg │ ├── 912ec66d7572ff821749319396470bde.svg │ ├── b06871f281fee6b241d60582ae9369b9.ttf │ ├── bundle.js │ └── index.html ├── index.html ├── index.js ├── media │ ├── favicon.png │ ├── header.jpg │ ├── header.png │ ├── images │ │ └── logo.png │ ├── logo_128x128.png │ ├── logo_dm.png │ ├── nulis.png │ ├── screenshot-1.png │ ├── screenshot-3.png │ ├── social-leaderboard.png │ ├── social-prompts.png │ └── social.png ├── reducers │ ├── #profiles.reducer.js# │ ├── index.js │ ├── preferences.reducer.js │ ├── profiles.reducer.js │ ├── tree.reducer.js │ └── trees.reducer.js ├── routes.js ├── styles │ ├── bootstrap.min.css │ ├── font-awesome.min.css │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ ├── fontawesome-webfont.woff2 │ │ ├── foundation-icons.eot │ │ ├── foundation-icons.svg │ │ ├── foundation-icons.ttf │ │ ├── foundation-icons.woff │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── foundation-icons.css │ ├── simplemde.min.css │ └── style.scss ├── utils │ ├── cards.js │ ├── dragAndDrop.js │ ├── handleScroll.js │ └── misc.js └── webpack.config.js ├── config ├── #settings.js# ├── .#settings.js ├── config.js ├── nulis_nginx.conf └── settings.js ├── desktop ├── 674f50d287a8c48dc19ba404d20fe713.eot ├── 89889688147bd7575d6327160d64e760.svg ├── 912ec66d7572ff821749319396470bde.svg ├── b06871f281fee6b241d60582ae9369b9.ttf ├── dist │ └── bundle.js ├── index.html ├── index.js ├── install.sh ├── main.js ├── package.json ├── styles │ ├── bootstrap.min.css │ └── style.css ├── utils │ └── index.js └── webpack.config.js ├── docker-compose.yml ├── package.json └── server ├── .babelrc ├── controllers ├── profiles.controllers.js └── tree.controllers.js ├── dist └── server.bundle.js ├── index.js ├── initialData.js ├── log.txt ├── misc ├── #hotprompts.py# ├── config.json ├── hotprompts.js ├── hotprompts.json ├── hotprompts.py ├── processed_authors_names_week.db ├── processed_names.db ├── top_authors_all.json ├── top_authors_week.json ├── topauthors.py ├── topauthors_back.py └── topauthors_bk.py ├── models ├── #tree.js# ├── tree.js └── user.js ├── nodemon.json ├── routes ├── profiles.routes.js └── trees.routes.js ├── server.js ├── services └── passport.js ├── static ├── js │ ├── bootstrap.min.js │ ├── jquery.min.js │ └── tether.min.js └── styles │ ├── .sass-cache │ ├── 06808200605fc810748fdc2898494e825f912dd6 │ │ ├── _bootstrap.min.scssc │ │ ├── _font-awesome.min.scssc │ │ ├── _foundation-icons.scssc │ │ ├── _opensans.scssc │ │ ├── _simplemde.min.scssc │ │ └── style.scssc │ └── ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf │ │ ├── _bootstrap.min.scssc │ │ ├── _font-awesome.min.scssc │ │ ├── _foundation-icons.scssc │ │ ├── _opensans.scssc │ │ ├── _simplemde.min.scssc │ │ └── style.scssc │ ├── _bootstrap.min.scss │ ├── _font-awesome.min.scss │ ├── _foundation-icons.scss │ ├── _opensans.scss │ ├── _simplemde.min.scss │ ├── config.rb │ ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ ├── foundation-icons.eot │ ├── foundation-icons.svg │ ├── foundation-icons.ttf │ ├── foundation-icons.woff │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 │ ├── style.css │ └── style.scss ├── views ├── #listprompts.ejs# ├── elements │ ├── footer.ejs │ ├── head.ejs │ └── header.ejs ├── leaderboard.ejs ├── listauthors.ejs ├── listprompts.ejs └── prompts.ejs ├── webpack.config.babel.js └── webpack.config.server.js /.dockerignore: -------------------------------------------------------------------------------- 1 | desktop 2 | .git 3 | node_modules 4 | node_modules_back 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | node_modules_back 3 | npm-debug.log 4 | desktop/builds/ 5 | desktop/packages/ 6 | Nulis-linux-x64.zip 7 | data/ 8 | config.js 9 | *.pkl 10 | 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | MAINTAINER Ray ALez 3 | 4 | # Setup environment variables containing paths 5 | ENV HOMEDIR=/home 6 | ENV PROJECT_DIR=/home/nulis 7 | ENV CLIENT_DIR=/home/nulis/client 8 | ENV SERVER_DIR=/home/nulis/server 9 | 10 | # Copy project files into /home/nulis folder. 11 | RUN mkdir -p $PROJECT_DIR 12 | WORKDIR $PROJECT_DIR 13 | COPY . . 14 | 15 | # Install npm modules 16 | RUN npm install --production 17 | 18 | # Port to expose 19 | EXPOSE 3000 20 | 21 | CMD npm run serve 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **https://nulis.io** 4 | 5 | Nulis is an open source tree editor for writers, inspired by [Gingko](https://gingkoapp.com/). 6 | 7 | Nulis allows you to represent any information as trees, beginning with abstract ideas and refining them by adding nested cards that contain more details. This format is very useful for writing stories, articles, outlining courses, GTD, and many other purposes. I think that organizing your creative projects in such way is an incredibly powerful idea, and I want more people to be able to apply use it in their own process. 8 | 9 | I built Nulis with Node/React/Redux, so that many developers would be able to understand the code and easily customize it for their own purposes. 10 | 11 | I hope that you will find it useful, as a writing tool, or just as an example of building a pretty cool SaaS with this tech =) 12 | -------------------------------------------------------------------------------- /assets/examples/dm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/dm.png -------------------------------------------------------------------------------- /assets/examples/gnk1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/gnk1.png -------------------------------------------------------------------------------- /assets/examples/gnk2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/gnk2.png -------------------------------------------------------------------------------- /assets/examples/goals1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/goals1.png -------------------------------------------------------------------------------- /assets/examples/goals2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/goals2.png -------------------------------------------------------------------------------- /assets/examples/goals3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/goals3.png -------------------------------------------------------------------------------- /assets/examples/interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/interface.png -------------------------------------------------------------------------------- /assets/examples/mast1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/mast1.png -------------------------------------------------------------------------------- /assets/examples/mast2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/mast2.png -------------------------------------------------------------------------------- /assets/examples/nulis-screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/nulis-screenshot1.png -------------------------------------------------------------------------------- /assets/examples/nulis-screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/nulis-screenshot2.png -------------------------------------------------------------------------------- /assets/examples/pricing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/pricing.png -------------------------------------------------------------------------------- /assets/examples/pricing1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/pricing1.png -------------------------------------------------------------------------------- /assets/examples/pricing2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/pricing2.png -------------------------------------------------------------------------------- /assets/examples/pricing3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/pricing3.png -------------------------------------------------------------------------------- /assets/examples/pricing4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/pricing4.png -------------------------------------------------------------------------------- /assets/examples/pricing5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/pricing5.png -------------------------------------------------------------------------------- /assets/examples/scrum1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/scrum1.pdf -------------------------------------------------------------------------------- /assets/examples/scrum2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/scrum2.png -------------------------------------------------------------------------------- /assets/examples/scrum3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/scrum3.png -------------------------------------------------------------------------------- /assets/examples/scrum4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/scrum4.png -------------------------------------------------------------------------------- /assets/examples/scrum5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/scrum5.png -------------------------------------------------------------------------------- /assets/examples/scrum6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/scrum6.png -------------------------------------------------------------------------------- /assets/examples/template1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/template1.png -------------------------------------------------------------------------------- /assets/examples/template2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/template2.png -------------------------------------------------------------------------------- /assets/examples/text.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/text.jpg -------------------------------------------------------------------------------- /assets/examples/text2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/text2.jpg -------------------------------------------------------------------------------- /assets/examples/text3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/text3.png -------------------------------------------------------------------------------- /assets/examples/text4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/text4.png -------------------------------------------------------------------------------- /assets/examples/true-novelist_796294_full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/true-novelist_796294_full.jpg -------------------------------------------------------------------------------- /assets/examples/tumblr_inline_mpogqunvFj1qz4rgp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/examples/tumblr_inline_mpogqunvFj1qz4rgp.png -------------------------------------------------------------------------------- /assets/history-v0.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/history-v0.1.png -------------------------------------------------------------------------------- /assets/logo_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/logo_128x128.png -------------------------------------------------------------------------------- /assets/logo_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/logo_full.png -------------------------------------------------------------------------------- /assets/logo_giant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/logo_giant.png -------------------------------------------------------------------------------- /assets/logo_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/logo_text.png -------------------------------------------------------------------------------- /assets/rsz_screenshot-10-stats2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/rsz_screenshot-10-stats2.png -------------------------------------------------------------------------------- /assets/rsz_screenshot-11-leaderboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/rsz_screenshot-11-leaderboard.png -------------------------------------------------------------------------------- /assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/screenshot-1.png -------------------------------------------------------------------------------- /assets/screenshot-10-stats2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/screenshot-10-stats2.png -------------------------------------------------------------------------------- /assets/screenshot-11-story-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/screenshot-11-story-template.png -------------------------------------------------------------------------------- /assets/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/screenshot-2.png -------------------------------------------------------------------------------- /assets/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/screenshot-3.png -------------------------------------------------------------------------------- /assets/screenshot-4-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/screenshot-4-search.png -------------------------------------------------------------------------------- /assets/screenshot-5-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/screenshot-5-mobile.png -------------------------------------------------------------------------------- /assets/screenshot-6-checkboxes-and-colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/screenshot-6-checkboxes-and-colors.png -------------------------------------------------------------------------------- /assets/screenshot-7-tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/screenshot-7-tags.png -------------------------------------------------------------------------------- /assets/screenshot-8-stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/screenshot-8-stats.png -------------------------------------------------------------------------------- /assets/screenshot-9-prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/screenshot-9-prompt.png -------------------------------------------------------------------------------- /assets/social-leaderboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/social-leaderboard.png -------------------------------------------------------------------------------- /assets/social-prompts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/social-prompts.png -------------------------------------------------------------------------------- /assets/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/assets/social.png -------------------------------------------------------------------------------- /assets/trees/blank.nls: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "", 3 | "name": "", 4 | "source":"Template", 5 | "cards": { 6 | "id": "root", 7 | "children": [ 8 | { 9 | "id": "0", 10 | "content": "", 11 | "children": [], 12 | "parent_id": "root" 13 | } 14 | ] 15 | }, 16 | "activeCard": "0", 17 | "modified": false, 18 | "editing": true 19 | } -------------------------------------------------------------------------------- /assets/trees/community.nls: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "590b5d2d870356725aa406cf", 3 | "slug": "community-9c0la3w", 4 | "name": "Community", 5 | "cards": { 6 | "id": "root", 7 | "children": [ 8 | { 9 | "id": "0", 10 | "content": "Community", 11 | "children": [], 12 | "parent_id": "root" 13 | } 14 | ] 15 | }, 16 | "author": "raymestalez@gmail.com", 17 | "__v": 0, 18 | "editing": true, 19 | "modified": false, 20 | "activeCard": "0", 21 | "updatedAt": null, 22 | "createdAt": null, 23 | "saved": true 24 | } -------------------------------------------------------------------------------- /assets/trees/nulis2.nls: -------------------------------------------------------------------------------- 1 | {"slug":"","name":"Nulis","cards":{"id":"root","children":[{"id":"3-x313781","content":"# Features","children":[{"id":"4-t2237p2","content":"# Big","children":[{"id":"6-6q4376w","content":"- [ ] Export markdown\n- [ ] Card splitting and merging\n","children":[]}]},{"id":"7-wv537st","content":"# Small","children":[{"id":"8-gm637jt","content":"- [ ] Autosave into db when card changes..\n- [ ] Probably shouldn't display droptargets on parents/children at all.\n- [ ] Horizontal scrolling.\n- [ ] Restore password\n- [ ] Move away from unsaved tree confirmation.\n- - Bind esc to preview mode, bind del to deleting.","children":[]}]},{"id":"9-tl737a1","content":"# Bugs","children":[{"id":"10-mi837mb","content":"- Will need to refactor header.","children":[]}]},{"id":"11-iv937v4","content":"# DevOps","children":[{"id":"12-owa37ll","content":"- Log errors.\n- Autorestart server when it crashes.\n- CI. proper deployment/testing and crap.\n","children":[]}]},{"id":"13-urb37yh","content":"# Desktop","children":[]},{"id":"5-xj337lc","content":"# Future/Maybe","children":[{"id":"14-wgc37no","content":"- Focus on subtree, goalscape style. Workflowy-like breadcrumb of parents.\n- Up/Down between card groups\n- Gear in the card to change color/setting.\n- Nulis prefs. Max columns, what else?\n- copy tree\n- Open Recent menu.\n- Cut/Paste cards/subtrees.\n- Collapse branches/cards.\n- Go to beginning/end of group/column\n Move to the beginning/end of the group.\n- Custom hotkeys\n- Create custom templates\n- Backend/Frontend apis. Auto-generate trees?\n- MaxColumns setting\n- word count\n- Insert snippets\n- Tree has stylesheet field for custom styles/themes.\n- Export presentation\n- Backups of everything\n- Code syntax highlighting. Latex/Mathjax.\n- ToDo items.\n- Tables. Embed html plots https://plot.ly/.\n- Zoom\n- Android app\n- Sync with dropbox\n- Plugins/addons\n- Themes. Dark.\n- Link to a card.\n- Tutorial(guiding you through clicks, like screeps)\n- Hide menu in fullscreen mode - just cards and hotkeys.\n- Draw.io revision history\n- Gingko upload image\n- Published tree settings.\n- Tags\n- Mobile for reals - bar with actions.\n","children":[]}]}]},{"id":"17-lef37zu","content":"# Trees","children":[{"id":"18-n7g37ps","content":"- AI resources.\n- Webdev resources.\n- AI/RAM breakdown.\n- Snowflake method.\n- Community breakdown\n- Comedy process+post.\n- All startup lecture notes!!!!! takeaways.\n//AP resources. \n","children":[]}]},{"id":"19-4bh372l","content":"# Blog Posts","children":[{"id":"20-3hi377j","content":"- Setup a vertex blog\n- Why am I building nulis?\n Tools for creators, bicycle for the mind. \n Fiction/Comedy as terminal value.\n Path to mastery. Microsteps + Sustainable Loops. My latest fiction epiphanies.\n Not a pretty interface but a framework/mission, that enales you to be a writer.\n- Flow\n- Story structure\n- creativity. va. applying patterns.\n- more ideas. what did I want from fhi? OM? metabook? It ALL could be HERE. \n On creativity.\n","children":[]}]},{"id":"21-97j37vb","content":"# Community","children":[{"id":"22-rhk377q","content":"Reddit? forum?\n- **User interviews.**\n - Think on and list optimal questions.\n - Proactively ask users specific things, intentional, high leverage.\n- Targeted emails/pms to writers.\n","children":[]}]},{"id":"23-3wl3760","content":"# High level. Idea. Meta-startup.","children":[{"id":"24-erm3767","content":"- Mission/Pupropse.\n- Value prop.\n- Feature ideas. HOW do I do better than gnk?\n","children":[]}]},{"id":"25-ucn37j1","content":"# Fiction","children":[{"id":"26-jbo377p","content":"- Swing back. Drop comics. Back to core ambition. My old goals? ephiphanies?","children":[]}]},{"id":"15-gld37pz","content":"# Growth/Traction","children":[{"id":"16-abe37v2","content":"","children":[]}]},{"id":"0","content":"# Startup School Notes\nCurriculum:\n- How/Why","children":[{"id":"2-lh037vl","content":"# How/Why\n- Figure out a mission\n- Be willing to drop 10 years on that. ","children":[]},{"id":"27-hkp3720","content":"- Short cycle time","children":[]}],"parent_id":"root"}]},"activeCard":"8-gm637jt","modified":false,"editing":true,"debugging":"{\"id\":\"8-gm637jt\",\"content\":\"- [ ] Autosave into db when card changes..\\n- [ ] Probably shouldn't display droptargets on parents/children at all.\\n- [ ] Horizontal scrolling.\\n- [ ] Restore password\\n- [ ] Move away from unsaved tree confirmation.\\n- - Bind esc to preview mode, bind del to deleting.\",\"children\":[]}","saved":false,"query":""} -------------------------------------------------------------------------------- /assets/trees/prompt.nls: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "", 3 | "name": "Prompt", 4 | "cards": { 5 | "id": "root", 6 | "children": [ 7 | { 8 | "id": "2-qv036lw", 9 | "content": "Prompt/Premise\nSciFi premise causes problems.\n\nAsk questions.\nAnswer with tropes.\nLists of five.", 10 | "children": [ 11 | { 12 | "id": "3-eg136or", 13 | "content": "Setting/Worldbuilding", 14 | "children": [] 15 | }, 16 | { 17 | "id": "4-kl236j8", 18 | "content": "Characters\nAge/gender/looks\nPersonality trope, \nProfession\nAntagonist", 19 | "children": [] 20 | }, 21 | { 22 | "id": "5-wd336i0", 23 | "content": "Challenge(conflict)/Resolution", 24 | "children": [] 25 | } 26 | ] 27 | }, 28 | { 29 | "id": "6-ef4362b", 30 | "content": "Plot.", 31 | "children": [ 32 | { 33 | "id": "7-rv5361r", 34 | "content": "[ ] Setup\nSetting, action, dialogue.\nObjective, conflict, change in value, infodump.\nPrecise outline if needed, what needs to happen.", 35 | "children": [ 36 | { 37 | "id": "11-ky936rb", 38 | "content": "Improv. In their heads. Straightforward description.", 39 | "children": [] 40 | } 41 | ] 42 | }, 43 | { 44 | "id": "10-8p836m6", 45 | "content": "[ ] Challenge/problem", 46 | "children": [] 47 | }, 48 | { 49 | "id": "8-8p636lq", 50 | "content": "[ ] Escalate\nProgress/Setbacks, 3 attempts, \nrandom encounters, plan stages", 51 | "children": [] 52 | }, 53 | { 54 | "id": "9-nl736ou", 55 | "content": "[ ] Resolve\n(change in value)", 56 | "children": [] 57 | } 58 | ] 59 | }, 60 | { 61 | "id": "12-8la36g5", 62 | "content": "Jokes", 63 | "children": [ 64 | { 65 | "id": "13-6zb36oy", 66 | "content": "Broad Description / Jump\n- Dot Connect. 2nd pattern that fits just extracted thing.\n ++ 2nd association chain back.", 67 | "children": [ 68 | { 69 | "id": "14-ndc364g", 70 | "content": "Second > First / Between dotconnects\nWrite 500 char comics script.\n\n++ Can make even simple premise much better by adding more tags, specific absurd examples.", 71 | "children": [] 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | ] 78 | }, 79 | "author": "raymestalez@gmail.com", 80 | "__v": 0, 81 | "editing": true, 82 | "modified": false, 83 | "activeCard": "2-qv036lw", 84 | "updatedAt": "2017-05-15T01:09:06.461Z", 85 | "createdAt": "2017-05-14T01:50:49.445Z", 86 | "saved": true, 87 | "source": "Template", 88 | "scroll": true 89 | } -------------------------------------------------------------------------------- /assets/trees/sharks.nls: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "590eb9cca942390019ec53f3", 3 | "slug": "", 4 | "name": "My next brilliant story", 5 | "cards": { 6 | "id": "root", 7 | "children": [ 8 | { 9 | "id": "0", 10 | "content": "# My next brilliant story\nWhat if sharks could ride lasor-raptors, and had to defent the white house against the fleet of alien spaceships?", 11 | "children": [ 12 | { 13 | "id": "2-0c036a1", 14 | "content": "# Act 1 - Aliens Attack\nThe peaceful world of shark-raptors has been disturbed by a horrible event...", 15 | "children": [ 16 | { 17 | "id": "5-ko336yn", 18 | "content": "# Scene One\nAnd so it begins.... The war between worlds, stopped by the unlikeliest of love stories.", 19 | "children": [] 20 | }, 21 | { 22 | "id": "6-8t436ya", 23 | "content": "# Scene two", 24 | "children": [] 25 | }, 26 | { 27 | "id": "7-ri536v3", 28 | "content": "# Scene three", 29 | "children": [] 30 | } 31 | ] 32 | }, 33 | { 34 | "id": "3-u61369z", 35 | "content": "# Act 2 - Trials and Tribulations\nIt all would be lost if one young rule-breaking raptor would not fall in love with a pretty career-oriented shark.", 36 | "children": [] 37 | }, 38 | { 39 | "id": "4-e0236y8", 40 | "content": "# Act 3 - The end of tomorrow\nThe final battle was almost lost until the psychic dolphins came to the resque.", 41 | "children": [] 42 | } 43 | ], 44 | "parent_id": "root" 45 | } 46 | ] 47 | }, 48 | "author": "raymestalez@gmail.com", 49 | "__v": 0, 50 | "editing": true, 51 | "modified": false, 52 | "activeCard": "6-8t436ya", 53 | "updatedAt": "2017-05-07T06:11:33.521Z", 54 | "createdAt": "2017-05-07T06:08:12.819Z", 55 | "saved": false, 56 | "debugging": "{\"id\":\"5-ko336yn\",\"content\":\"# Scene One\\nAnd so it begins.... The war between worlds, stopped by the unlikeliest of love stories.\",\"children\":[]}" 57 | } -------------------------------------------------------------------------------- /assets/trees/story_back.nls: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "590a31dd348beb446dd319d4", 3 | "name": "Story", 4 | "source":"Template", 5 | "cards": { 6 | "id": "root", 7 | "children": [ 8 | { 9 | "id": "0", 10 | "content": "# Worldbuilding", 11 | "children": [ 12 | { 13 | "id": "4-3233z0y", 14 | "content": "# Premise\nGeneral idea of the world of your story.", 15 | "children": [ 16 | { 17 | "id": "5-me43z47", 18 | "content": "# Physical. Social/Cultural.", 19 | "children": [] 20 | }, 21 | { 22 | "id": "6-9p53z3k", 23 | "content": "# Magic system. \nWhat if. Rules and limitations.", 24 | "children": [] 25 | }, 26 | { 27 | "id": "7-sz63zsh", 28 | "content": "# Artifacts/spells/superpowers", 29 | "children": [] 30 | }, 31 | { 32 | "id": "8-dp73zg3", 33 | "content": "# Settings\nDifferent locations in your world.", 34 | "children": [] 35 | } 36 | ] 37 | } 38 | ], 39 | "parent_id": "root" 40 | }, 41 | { 42 | "id": "2-ub13z6o", 43 | "content": "# Characters", 44 | "children": [ 45 | { 46 | "id": "9-fx83zae", 47 | "content": "**Character Name**\nBrief description.", 48 | "children": [ 49 | { 50 | "id": "10-vt93z9d", 51 | "content": "Character Details", 52 | "children": [] 53 | } 54 | ] 55 | } 56 | ] 57 | }, 58 | { 59 | "id": "3-5a23zzl", 60 | "content": "# Plot outline\nGenereal idea of what is going to happen in your story.", 61 | "children": [ 62 | { 63 | "id": "11-8ka3z1l", 64 | "content": "# Chapter/Episode 1\nEpisode Description (tagline).\n\n## Act 1 - Setup\nA story:\nB story:\n## Act 2 - Escalate\nA story:\nB story:\n## Act 3 - Resolve\nA story:\nB story:", 65 | "children": [ 66 | { 67 | "id": "12-99b3ztf", 68 | "content": "# Scene 1\nSetting\nDescription\nAction\nDialogue\n>< Conflict\n+/- Change of value", 69 | "children": [] 70 | }, 71 | { 72 | "id": "13-9ac3zv1", 73 | "content": "# Scene 2\n....", 74 | "children": [] 75 | }, 76 | { 77 | "id": "14-idd3zm1", 78 | "content": "", 79 | "children": [] 80 | } 81 | ] 82 | }, 83 | { 84 | "id": "15-sq030yp", 85 | "content": "**Episode concepts**\nAdd more cards here to outline the rest of your chapters or episodes for this season.", 86 | "children": [] 87 | } 88 | ] 89 | } 90 | ] 91 | }, 92 | "__v": 0, 93 | "author": "raymestalez3@gmail.com", 94 | "editing": true, 95 | "modified": false, 96 | "activeCard": "0", 97 | "updatedAt": null, 98 | "createdAt": null 99 | } -------------------------------------------------------------------------------- /assets/trees/test.nls: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "590e0ad3f5916510d40627c2", 3 | "slug": "", 4 | "name": "Test", 5 | "cards": { 6 | "id": "root", 7 | "children": [ 8 | { 9 | "id": "0", 10 | "content": "Test dsd", 11 | "children": [ 12 | { 13 | "id": "3-9r13749", 14 | "content": "# Another List\n- [ ] Hey\n- [X] There\n- [ ] Buddys", 15 | "children": [] 16 | }, 17 | { 18 | "id": "3-zh537tw", 19 | "content": "", 20 | "children": [] 21 | } 22 | ], 23 | "parent_id": "root" 24 | }, 25 | { 26 | "id": "5-uw1377o", 27 | "content": "", 28 | "children": [] 29 | }, 30 | { 31 | "id": "4-9k037t6", 32 | "content": "", 33 | "children": [] 34 | } 35 | ] 36 | }, 37 | "author": "raymestalez@gmail.com", 38 | "__v": 0, 39 | "editing": true, 40 | "modified": false, 41 | "activeCard": "5-uw1377o", 42 | "updatedAt": "2017-05-08T01:28:47.410Z", 43 | "createdAt": null, 44 | "saved": true, 45 | "source": "Online" 46 | } -------------------------------------------------------------------------------- /assets/trees/test2.nls: -------------------------------------------------------------------------------- 1 | { 2 | "name": "About", 3 | "slug": "", 4 | "cards": { 5 | "id": "root", 6 | "children": [ 7 | { 8 | "id": "0", 9 | "content": "![](/media/header.png)\n\n# Welcome to Nulis\nThis is a tree editor for writers. It makes the process of outlining your stories, articles, or books easy, fun, and convenient.\n\nFeel free to edit this page to experiment with it, don't worry, it's a template, you can always reopen it from the `About` menu.\n\nPress `Ctrl+Enter` to try editing cards, click `+` buttons to create new ones, drag cards by the handle(to the left of the card) to rearrange them.\n\nRead on to the second column to learn more.\n\n#three #two", 10 | "children": [ 11 | { 12 | "creator_id": "0", 13 | "content": "# How it works\nNulis is useful for organizing large amounts of information, breaking down your large writing goals into smaller steps, and working on your writing in manageable chunks. The purpose of Nulis is to help you to write in the state of flow.\n\nNulis allows you to edit your story on multiple different levels. Describe the general idea of your story or an article in the first column, then create child cards to the right of it to add more details. \n\nFor example cards in the second column can describe your story outline, and cards in the third column can describe your scenes.\n\n#one #two", 14 | "position": "right", 15 | "children": [ 16 | { 17 | "creator_id": 2, 18 | "content": "# Hotkeys\nTo edit the tree, you need to remember two sets of keys: \n\n- Directions(can also just use arrows): \n `H` - left \n\t`J` - down \n\t`K` - up \n\t`L` - right \n- Actions: \n `Ctrl` - select \n\t`Ctrl+Shift` - create \n\t`Alt` - move \n\t\nYou can combine them to form shortcuts. So for example to create a card to the right, you would press `Ctrl+Shift+L`, and to move the card down you'd press `Alt+J`. Try it now!\n\nTo switch between editing/preview modes use `Ctrl+Enter`. Double click and `Enter` also work for entering the edit mode.\n\n`Alt+F` toggles the fullscreen mode in the editor.\n\nAlso, you can delete cards with `Ctrl+Backspace`, and undo things with `Ctrl-Z`.", 19 | "position": "after", 20 | "children": [], 21 | "id": 3, 22 | "parent_id": 2 23 | }, 24 | { 25 | "id": "6-pd13zce", 26 | "content": "# Upcoming features\n- Search.\n- Tags.\n- Conveniently splitting and merging cards.\n- Collapse branches/cards (like in mindmaps).\n- Focus on a subtree.\n- Cut/Paste trees and cards.\n- Export markdown.\n- Possibly plugins and themes.\n- And much more =)", 27 | "children": [] 28 | } 29 | ], 30 | "id": 2, 31 | "parent_id": "0" 32 | }, 33 | { 34 | "creator_id": 3, 35 | "content": "# Examples\nOpen the `Tree` menu and select one of the templates at the bottom to browse some examples of what can be done in Nulis.\n\n#one", 36 | "position": "after", 37 | "children": [ 38 | { 39 | "id": "7-xv43z7n", 40 | "content": "# Snowflake method\nNulis can be extremely convenient for writing stories using a [\"Snowflake Method\"](http://www.advancedfictionwriting.com/articles/snowflake-method/).\n", 41 | "children": [] 42 | } 43 | ], 44 | "id": 4, 45 | "parent_id": "0" 46 | }, 47 | { 48 | "id": "6-sz03zdq", 49 | "content": "# Pricing\nThis is a very early beta version of this app, so using it is completely free. When it will become more mature, the premium version will cost $5 per month, or $50 per year, or $150 one time payment.\n\nIt will also be possible to get Nulis for free, by inviting other users, or by providing useful feedback.", 50 | "children": [] 51 | } 52 | ], 53 | "parent_id": "root" 54 | } 55 | ] 56 | }, 57 | "modified": false, 58 | "activeCard": 4, 59 | "updatedAt": null, 60 | "createdAt": null, 61 | "editing": false, 62 | "debugging": "\"one\"", 63 | "query": "one" 64 | } 65 | -------------------------------------------------------------------------------- /assets/trees/test3.nls: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "", 3 | "name": "adfasdf", 4 | "cards": { 5 | "id": "root", 6 | "children": [ 7 | { 8 | "id": "0", 9 | "content": "adfasdf asd", 10 | "children": [], 11 | "parent_id": "root" 12 | } 13 | ] 14 | }, 15 | "author": "test5@gmail.com", 16 | "__v": 0, 17 | "editing": true, 18 | "modified": false, 19 | "activeCard": "0", 20 | "updatedAt": "2017-05-09T04:20:36.397Z", 21 | "createdAt": "2017-05-09T04:20:36.397Z", 22 | "saved": false, 23 | "source": "Online", 24 | "debugging": "{\"id\":\"0\",\"content\":\"adfasdf asd\",\"children\":[],\"parent_id\":\"root\"}" 25 | } -------------------------------------------------------------------------------- /assets/trees/writingstreak.nls: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "", 3 | "name": "Prompt", 4 | "cards": { 5 | "id": "root", 6 | "children": [ 7 | { 8 | "id": "2-qv036lw", 9 | "content": "Prompt/Premise\nSciFi premise causes problems.\n\nAsk questions.\nAnswer with tropes.\nLists of five.", 10 | "children": [ 11 | { 12 | "id": "3-eg136or", 13 | "content": "Setting/Worldbuilding ", 14 | "children": [] 15 | }, 16 | { 17 | "id": "4-kl236j8", 18 | "content": "Characters\nAge/gender/looks\nPersonality trope, \nProfession\nAntagonist", 19 | "children": [] 20 | }, 21 | { 22 | "id": "5-wd336i0", 23 | "content": "Challenge(conflict)/Resolution", 24 | "children": [] 25 | } 26 | ] 27 | }, 28 | { 29 | "id": "6-ef4362b", 30 | "content": "Plot.", 31 | "children": [ 32 | { 33 | "id": "7-rv5361r", 34 | "content": "[ ] Setup\nSetting, action, dialogue.\nObjective, conflict, change in value, infodump.\nPrecise outline if needed, what needs to happen.\n\nWrite a paragraph about the world where the story takes place.\n\nDescribe a character in a situation.", 35 | "children": [ 36 | { 37 | "id": "11-ky936rb", 38 | "content": "Improv. In their heads. Straightforward description.", 39 | "children": [] 40 | } 41 | ] 42 | }, 43 | { 44 | "id": "10-8p836m6", 45 | "content": "[ ] Challenge/problem\n\nDescribe the event that disrupts the normal course of events and begins the story. What can go wrong? What problem is character facing? What goal does he have? Who is the antagonist?", 46 | "children": [] 47 | }, 48 | { 49 | "id": "8-8p636lq", 50 | "content": "[ ] Escalate\nProgress/Setbacks, 3 attempts, \nrandom encounters, plan stages\n\nHow can things escalate? How does the character pursue his goal, what obstacles/challenes does he need to overcome? How can things spiral out of control, raising the stakes, requiring bigger and bigger risks, leading to the showdown between the hero and antagonist?", 51 | "children": [] 52 | }, 53 | { 54 | "id": "9-nl736ou", 55 | "content": "[ ] Resolve\nHow does it end? Does the character win? How? Why?\nDid the world go back to normal? What changed?\n(change in value)", 56 | "children": [] 57 | } 58 | ] 59 | }, 60 | { 61 | "id": "12-8la36g5", 62 | "content": "Jokes", 63 | "children": [ 64 | { 65 | "id": "13-6zb36oy", 66 | "content": "Broad Description / Jump\n- Dot Connect. 2nd pattern that fits just extracted thing.\n ++ 2nd association chain back.", 67 | "children": [ 68 | { 69 | "id": "14-ndc364g", 70 | "content": "Second > First / Between dotconnects\nWrite 500 char comics script.\n\n++ Can make even simple premise much better by adding more tags, specific absurd examples.", 71 | "children": [] 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | ] 78 | }, 79 | "author": "raymestalez@gmail.com", 80 | "__v": 0, 81 | "editing": true, 82 | "modified": false, 83 | "activeCard": "9-nl736ou", 84 | "updatedAt": "2017-05-15T01:09:06.461Z", 85 | "createdAt": "2017-05-14T01:50:49.445Z", 86 | "saved": false, 87 | "source": "Template", 88 | "scroll": false 89 | } -------------------------------------------------------------------------------- /client/#index.html#: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nulis - a tree editor for writers 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/#webpack.config.js#: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | function getPlugins() { 4 | const plugins = []; 5 | 6 | if (process.env.NODE_ENV === "development") { 7 | plugins.push(new webpack.DefinePlugin({ 8 | 'process.env.NODE_ENV': JSON.stringify('development') 9 | })); 10 | } 11 | if (process.env.NODE_ENV === "production") { 12 | plugins.push(new webpack.DefinePlugin({ 13 | 'process.env': { 14 | NODE_ENV: JSON.stringify('production') 15 | } 16 | })); 17 | plugins.push(new webpack.optimize.UglifyJsPlugin({ 18 | minimize: true, 19 | output: { 20 | comments: false 21 | }, 22 | compressor: { 23 | warnings: false 24 | } 25 | })); 26 | } 27 | 28 | return plugins; 29 | } 30 | 31 | 32 | module.exports = { 33 | entry: [ 34 | './index.js' 35 | ], 36 | output: { 37 | path: __dirname + '/dist/', 38 | publicPath: '/', 39 | filename: 'bundle.js' 40 | }, 41 | devServer: { 42 | historyApiFallback: true, 43 | }, 44 | module: { 45 | loaders: [ 46 | { 47 | exclude: /node_modules/, 48 | loader: 'babel-loader', 49 | query: { 50 | presets: ['react', 'es2015', 'stage-1'], 51 | plugins: ["transform-decorators-legacy"] 52 | } 53 | }, 54 | { 55 | test: /\.scss$/, 56 | loaders: ['style-loader', 'css-loader', 'sass-loader'] 57 | }, 58 | { 59 | test: /\.css$/, 60 | loader: 'style-loader!css-loader' 61 | }, 62 | { 63 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 64 | loader: 'url-loader?limit=100000' 65 | }, 66 | { 67 | test: /\.json$/, 68 | loader: 'json-loader' 69 | }, 70 | { 71 | test: /\.nls/, 72 | loader: 'raw-loader' 73 | } 74 | ] 75 | }, 76 | plugins: getPlugins(), 77 | resolve: { 78 | extensions: ['.js', '.jsx'] 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-1"], 3 | "plugins": ["transform-decorators-legacy"] 4 | } 5 | -------------------------------------------------------------------------------- /client/actions/cards.actions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { browserHistory } from 'react-router'; 3 | 4 | import { DEFAULT_TREE } from '../data'; 5 | 6 | import { getCard } from '../utils/cards'; 7 | import { handleScroll } from '../utils/handleScroll'; 8 | 9 | var API_URL = 'https://nulis.io/api/v1'; 10 | if (process.env.NODE_ENV === 'development') { 11 | API_URL = 'http://localhost:3000/api/v1'; 12 | } 13 | console.log("API_URL " + API_URL); 14 | export {API_URL}; 15 | 16 | const config = { 17 | headers: { authorization: localStorage.getItem('token')} 18 | }; 19 | 20 | 21 | 22 | export function updateTreeName(value) { 23 | /* unused */ 24 | return { 25 | type: 'UPDATE_TREE_NAME', 26 | payload: value 27 | } 28 | } 29 | 30 | export function setCardColor(color) { 31 | /* unused */ 32 | return { 33 | type: 'SET_CARD_COLOR', 34 | payload: color 35 | } 36 | } 37 | export function setCardConfig(boolean) { 38 | /* unused */ 39 | return { 40 | type: 'SET_CARD_CONFIG', 41 | payload: boolean 42 | } 43 | } 44 | 45 | export function checkCheckbox(index, cardId) { 46 | /* unused */ 47 | return { 48 | type: 'CHECKBOX', 49 | payload: {index:index, cardId:cardId} 50 | } 51 | } 52 | 53 | 54 | 55 | export function createCard(direction, card) { 56 | var cardsCreated = 0; 57 | if(localStorage.getItem('cardsCreated')){ 58 | cardsCreated = parseInt(localStorage.getItem('cardsCreated')); 59 | } 60 | localStorage.setItem('cardsCreated', (cardsCreated+1)); 61 | 62 | return { 63 | type: 'CREATE_CARD', 64 | payload: {direction,card} 65 | } 66 | } 67 | 68 | export function dropCard(direction, relativeTo, card) { 69 | return { 70 | type: 'DROP_CARD', 71 | payload: {direction, relativeTo, card} 72 | } 73 | } 74 | 75 | export function updateCard(card, content) { 76 | var prevWordcount = card.content.split(' ').length; 77 | var currentWordcount = content.split(' ').length; 78 | card.content = content; 79 | return function(dispatch) { 80 | dispatch({ 81 | type: 'UPDATE_CARD', 82 | payload: card 83 | }); 84 | if (currentWordcount > prevWordcount) { 85 | dispatch({ 86 | type: 'ADD_WORD' 87 | }); 88 | } 89 | } 90 | } 91 | 92 | export function deleteCard(card) { 93 | /* console.log("Card deleted " + JSON.stringify(card)); */ 94 | return { 95 | type: 'DELETE_CARD', 96 | payload: card 97 | } 98 | } 99 | 100 | export function selectCard(direction) { 101 | return { 102 | type: 'SELECT_CARD', 103 | payload: direction 104 | } 105 | } 106 | 107 | 108 | export function moveCard(direction) { 109 | /* console.log("Moving card " + JSON.stringify(card)); */ 110 | return { 111 | type: 'MOVE_CARD', 112 | payload: direction 113 | } 114 | } 115 | 116 | export function setActiveCard(cardId) { 117 | return { 118 | type: 'SET_ACTIVE_CARD', 119 | payload: cardId 120 | } 121 | } 122 | 123 | -------------------------------------------------------------------------------- /client/actions/preferences.actions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { browserHistory } from 'react-router'; 3 | 4 | import {API_URL} from './cards.actions'; 5 | 6 | 7 | export function setShowModal(modal) { 8 | return { 9 | type: 'SET_SHOW_MODAL', 10 | payload: modal 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /client/actions/profiles.actions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { browserHistory } from 'react-router'; 3 | 4 | import {API_URL} from './cards.actions'; 5 | 6 | 7 | export function fetchUser() { 8 | const config = { 9 | headers: { authorization: localStorage.getItem('token')} 10 | }; 11 | 12 | /* console.log("profiles.actions:");*/ 13 | /* console.log("Fetching user.");*/ 14 | return function(dispatch) { 15 | axios.get(`${API_URL}/auth/profile`, config) 16 | .then(response => { 17 | /* console.log("profiles.actions:");*/ 18 | console.log("Fetched user " + JSON.stringify(response.data)); 19 | dispatch({ 20 | type: 'AUTH_USER', 21 | payload: response.data 22 | }); 23 | }); 24 | } 25 | } 26 | 27 | 28 | export function login(credentials) { 29 | return function(dispatch) { 30 | /* console.log("profiles.actions:");*/ 31 | /* console.log("Sending email and password.");*/ 32 | axios.post(`${API_URL}/auth/login`, credentials) 33 | .then(response => { 34 | /* console.log("profiles.actions:");*/ 35 | /* console.log("Sign in successful."); */ 36 | /*console.log("Saving user to state, saving token to local storage.");*/ 37 | console.log("Fetched user " + JSON.stringify(response.data)); 38 | dispatch({ 39 | type: 'AUTH_USER', 40 | payload: response.data 41 | }); 42 | localStorage.setItem('token', response.data.token); 43 | console.log("Redirecting to /"); 44 | browserHistory.push('/trees'); 45 | }) 46 | .catch((err) => { 47 | if (err) { 48 | console.log("profiles.actions:"); 49 | console.log("Login error " + err); 50 | dispatch({ 51 | type: 'AUTH_ERROR', 52 | payload: "Authentication error" 53 | }); 54 | } 55 | }) 56 | 57 | }; 58 | } 59 | 60 | 61 | export function join(credentials) { 62 | return function(dispatch) { 63 | console.log("profiles.actions:"); 64 | console.log("Sending email and password."); 65 | console.log("ref " + credentials.referral); 66 | console.log("src " + credentials.source); 67 | axios.post(`${API_URL}/auth/join`, credentials) 68 | .then(response => { 69 | console.log("profiles.actions:"); 70 | console.log("Created user."); 71 | console.log("Saving user to state, saving JWT token to local storage."); 72 | dispatch({ 73 | type: 'AUTH_USER', 74 | payload: response.data 75 | }); 76 | localStorage.setItem('token', response.data.token); 77 | 78 | console.log("Redirecting to /"); 79 | browserHistory.push('/trees'); 80 | }) 81 | .catch((err) => { 82 | if (err) { 83 | console.log("profiles.actions:"); 84 | console.log("Could not create user. " + err); 85 | dispatch({ 86 | type: 'AUTH_ERROR', 87 | payload: "Authentication error" 88 | }); 89 | } 90 | }) 91 | 92 | }; 93 | } 94 | 95 | 96 | 97 | export function logout() { 98 | // delete token and signout 99 | /* console.log("profiles.actions:");*/ 100 | console.log("Logging out.") 101 | /* console.log("Removing token from local storage, removing user from state.");*/ 102 | localStorage.removeItem('token'); 103 | localStorage.removeItem('cardsCreated'); 104 | return { 105 | type: 'UNAUTH_USER' 106 | }; 107 | 108 | console.log("Redirecting to /."); 109 | browserHistory.push('/'); 110 | } 111 | 112 | 113 | 114 | export function payment(token) { 115 | const config = { 116 | headers: { authorization: localStorage.getItem('token')} 117 | }; 118 | console.log("profiles.actions:"); 119 | console.log(`Sending payment token.`); 120 | return function(dispatch) { 121 | axios.post(`${API_URL}/purchase`, token, config) 122 | .then(response => { 123 | console.log("Payment successful"); 124 | console.log("Plan: " + response.data.user.plan); 125 | console.log(`Saving updated user to the state.`); 126 | dispatch({ 127 | type: 'AUTH_USER', 128 | payload: response.data.user 129 | }); 130 | }); 131 | } 132 | } 133 | 134 | 135 | export function updateWordcount(today) { 136 | const config = { 137 | headers: { authorization: localStorage.getItem('token')} 138 | }; 139 | return function(dispatch) { 140 | axios.post(`${API_URL}/update-wordcount`, today, config) 141 | .then(response => { 142 | /* Wordcount updated */ 143 | /* console.log(response.data.message);*/ 144 | }); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /client/components/#App.js#: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | /* Vendor */ 5 | import Cookies from "js-cookie"; 6 | 7 | /* Styles */ 8 | import '../styles/bootstrap.min.css'; 9 | import '../styles/font-awesome.min.css'; 10 | import '../styles/simplemde.min.css'; 11 | import '../styles/style.scss'; 12 | 13 | /* My Components */ 14 | import Header from './Header'; 15 | import Hotkeys from './Hotkeys'; 16 | 17 | /* Actions */ 18 | import { fetchUser } from '../actions/profiles.actions'; 19 | 20 | class App extends Component { 21 | componentDidMount() { 22 | if (localStorage.getItem('token')){ 23 | this.props.fetchUser(); 24 | } 25 | 26 | /* Save referral/source to cookies */ 27 | if (this.props.location.query.ref) { 28 | var referral = this.props.location.query.ref; 29 | /* Save refferral link cookie */ 30 | Cookies.set("referral", referral, {expires: 7}); 31 | } 32 | if (this.props.location.query.src) { 33 | var source = this.props.location.query.src; 34 | /* Save source link cookie */ 35 | Cookies.set("source", source, {expires: 7}); 36 | } 37 | 38 | } 39 | 40 | render() { 41 | const { children } = this.props; 42 | 43 | return ( 44 |
45 |
46 | { children } 47 | 48 |
49 | ); 50 | } 51 | } 52 | 53 | export default connect(null, { fetchUser })(App); 54 | -------------------------------------------------------------------------------- /client/components/#Card.js#: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | /* Vendor components */ 5 | import Remarkable from 'remarkable'; 6 | import { DragSource, DragTarget } from 'react-dnd'; 7 | 8 | /* Actions */ 9 | import * as cardsActions from '../actions/cards.actions'; 10 | import {setEditing} from '../actions/trees.actions'; 11 | 12 | /* Utils */ 13 | import { getAllChildren, isActive, getParent, getCard } from '../utils/cards'; 14 | import {cardSource} from '../utils/dragAndDrop'; 15 | 16 | /* Components */ 17 | import Editor from './Editor'; 18 | import ColorBox from './ColorBox'; 19 | import DropTarget from './DropTarget'; 20 | 21 | 22 | // Use decorator to make card draggable and pass it some functions. 23 | @DragSource('CARD', cardSource, (connect, monitor) => ({ 24 | connectDragSource: connect.dragSource(), 25 | connectDragPreview: connect.dragPreview(), 26 | isDragging: monitor.isDragging() 27 | })) 28 | class Card extends Component { 29 | componentDidMount(){ 30 | } 31 | 32 | componentDidUpdate(){ 33 | } 34 | 35 | render() { 36 | const { card } = this.props; 37 | const { isDragging, connectDragSource, connectDragPreview } = this.props; 38 | 39 | var active = isActive(card, this.props.tree.cards, this.props.tree.activeCard); 40 | 41 | return ( 42 |
this.props.setEditing(true)} 45 | style={(card.color?{borderLeft: `3px solid ${card.color}`}:null)} 46 | className={"card " 47 | + (active == "card" ? "active " : "") 48 | + (isDragging ? "dragging " : "") 49 | + (active == "children" ? "children-active" : "")}> 50 | {connectDragSource( 51 |
52 | )} 53 | {connectDragPreview(
)} 54 | 55 | 56 | 57 |
this.props.createCard("before", card)}>+
59 |
this.props.createCard("before", card)}> 61 | 62 |
63 | 64 |
this.props.createCard("right", card)}>+
66 |
this.props.createCard("after", card)}>+
68 |
this.props.deleteCard(card)}> 70 | ❌ 71 |
72 | 73 | 74 | 75 | 77 |
78 |
79 | 80 | Id: 81 | {card.id} 82 | 83 | 84 | Parent: 85 | {getParent(card, this.props.tree.cards).id} 86 | 87 |
88 |
89 | ); 90 | } 91 | } 92 | 93 | 94 | /* Connect Drag and Drop props to the component. */ 95 | function collect(connect, monitor) { 96 | return { 97 | connectDragSource: connect.dragSource(), 98 | isDragging: monitor.isDragging() 99 | }; 100 | } 101 | 102 | 103 | function mapStateToProps(state) { 104 | return { tree: state.tree.present }; 105 | } 106 | var card = connect(mapStateToProps, {...cardsActions, setEditing})(Card); 107 | 108 | /* Connect drag and drop to the card */ 109 | export default DragSource('CARD', cardSource, collect)(card); 110 | -------------------------------------------------------------------------------- /client/components/#ModalThankYou.js#: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | /* Vendor Components */ 6 | import { Modal } from 'react-bootstrap'; 7 | 8 | /* Actions */ 9 | import {setShowModal} from '../actions/preferences.actions'; 10 | 11 | 12 | class ModalFree extends Component { 13 | render() { 14 | return ( 15 | this.props.setShowModal(false)}> 18 | 19 |

Thank you!

20 |
21 |
22 |

Thank you so much for your support! =) 23 | I really hope that Nulis will be very useful to you.

24 |

If you have any questions, issues, or would like to 25 | suggest a feature - always feel free to send me an email to 26 | raymestalez@gmail.com

27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | 34 | function mapStateToProps(state) { 35 | return { 36 | showModal: state.preferences.showModal 37 | }; 38 | } 39 | 40 | export default connect(mapStateToProps, { setShowModal })(ModalFree); 41 | 42 | -------------------------------------------------------------------------------- /client/components/#Search.js#: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | import { browserHistory } from 'react-router'; 6 | import { Link } from 'react-router'; 7 | 8 | /* My Components */ 9 | 10 | /* Actions */ 11 | import * as treesActions from '../actions/trees.actions'; 12 | 13 | class Search extends Component { 14 | componentDidMount(){ 15 | /* Shortcuts */ 16 | /* Focus on search */ 17 | Mousetrap(document.body).bind(['ctrl+/'], ()=>{ 18 | ReactDOM.findDOMNode(this.refs.search).focus(); 19 | return false; 20 | }); 21 | } 22 | 23 | render () { 24 | return ( 25 | 26 | 29 | this.props.updateSearchQuery(event.target.value)}/> 30 | 31 | 32 | ); 33 | } 34 | } 35 | 36 | function mapStateToProps(state) { 37 | return { 38 | query: state.tree.present.query 39 | }; 40 | } 41 | 42 | export default connect(mapStateToProps, treesActions)(Search); 43 | 44 | -------------------------------------------------------------------------------- /client/components/#Stats.js#: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { browserHistory } from 'react-router'; 6 | import { Link } from 'react-router'; 7 | 8 | /* Vendor Components */ 9 | import { Modal } from 'react-bootstrap'; 10 | 11 | /* Actions */ 12 | import * as cardsActions from '../actions/cards.actions'; 13 | import {setShowModal} from '../actions/preferences.actions'; 14 | 15 | class Stats extends Component { 16 | renderCalendar() { 17 | return( 18 |
19 |
20 | {this.props.user.stats.streak} 21 |
22 |
23 |
24 | {this.props.user.stats.calendar.map((day)=> { 25 | var brightness = 0; 26 | if (day.wordcount > 100){ 27 | brightness = (day.wordcount+100)/1000; 28 | } 29 | return
32 | })} 33 |
34 |
35 |
36 | ); 37 | } 38 | 39 | renderWordcounter() { 40 | var calendar = this.props.user.stats.calendar 41 | var wordcount = calendar[calendar.length - 1].wordcount; 42 | 43 | return( 44 |
45 |
46 | {wordcount} 47 |
48 |
49 |
51 |
52 |
53 | {[...Array(10).keys()].map((i)=> { 54 | return
55 | })} 56 |
57 |
58 |
59 | ); 60 | } 61 | render() { 62 | if (!this.props.user ) { return null; } 63 | return( 64 |
this.props.setShowModal("stats")}> 66 | {this.renderCalendar()} 67 | {this.renderWordcounter()} 68 | this.props.setShowModal(false)}> 71 | 72 |

Stats

73 |
74 |
75 |

This is your writing stats.

76 |

The top bar is the calendar of the last 10 days, 77 | brightness of the day depends on the amount of words you have 78 | written(0 - completely white, 1000 - completely orange).

79 |

Number to the left of it is your current streak - how many days in a row you have written at least 100 words.

80 |

The bottom bar represents the number of words you wrote 81 | today.

82 |
83 |
84 | 85 |
86 | ); 87 | } 88 | } 89 | 90 | 91 | function mapStateToProps(state) { 92 | return { 93 | user: state.profiles.user, 94 | showModal: state.preferences.showModal 95 | }; 96 | } 97 | 98 | export default connect(mapStateToProps, {setShowModal})(Stats); 99 | 100 | -------------------------------------------------------------------------------- /client/components/.#ModalThankYou.js: -------------------------------------------------------------------------------- 1 | ray@lumen.3606:1494388886 -------------------------------------------------------------------------------- /client/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | /* Vendor */ 5 | import Cookies from "js-cookie"; 6 | 7 | /* Styles */ 8 | import '../styles/bootstrap.min.css'; 9 | import '../styles/font-awesome.min.css'; 10 | import '../styles/simplemde.min.css'; 11 | import '../styles/style.scss'; 12 | 13 | /* My Components */ 14 | import Header from './Header'; 15 | import Hotkeys from './Hotkeys'; 16 | 17 | /* Actions */ 18 | import { fetchUser } from '../actions/profiles.actions'; 19 | 20 | class App extends Component { 21 | componentDidMount() { 22 | if (localStorage.getItem('token')){ 23 | this.props.fetchUser(); 24 | } 25 | 26 | /* Save referral/source to cookies */ 27 | if (this.props.location.query.ref) { 28 | var referral = this.props.location.query.ref; 29 | /* Save refferral link cookie */ 30 | Cookies.set("referral", referral, {expires: 7}); 31 | } 32 | if (this.props.location.query.src) { 33 | var source = this.props.location.query.src; 34 | /* Save source link cookie */ 35 | Cookies.set("source", source, {expires: 7}); 36 | } 37 | 38 | } 39 | 40 | render() { 41 | const { children } = this.props; 42 | 43 | return ( 44 |
45 |
46 | { children } 47 | 48 |
49 | ); 50 | } 51 | } 52 | 53 | export default connect(null, { fetchUser })(App); 54 | -------------------------------------------------------------------------------- /client/components/Card.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | /* Vendor components */ 5 | import Remarkable from 'remarkable'; 6 | import { DragSource, DragTarget } from 'react-dnd'; 7 | 8 | /* Actions */ 9 | import * as cardsActions from '../actions/cards.actions'; 10 | import {setEditing} from '../actions/trees.actions'; 11 | 12 | /* Utils */ 13 | import { getAllChildren, isActive, getParent, getCard } from '../utils/cards'; 14 | import {cardSource} from '../utils/dragAndDrop'; 15 | 16 | /* Components */ 17 | import Editor from './Editor'; 18 | import ColorBox from './ColorBox'; 19 | import DropTarget from './DropTarget'; 20 | 21 | 22 | // Use decorator to make card draggable and pass it some functions. 23 | @DragSource('CARD', cardSource, (connect, monitor) => ({ 24 | connectDragSource: connect.dragSource(), 25 | connectDragPreview: connect.dragPreview(), 26 | isDragging: monitor.isDragging() 27 | })) 28 | class Card extends Component { 29 | componentDidMount(){ 30 | } 31 | 32 | componentDidUpdate(){ 33 | } 34 | 35 | render() { 36 | const { card } = this.props; 37 | const { isDragging, connectDragSource, connectDragPreview } = this.props; 38 | 39 | var active = isActive(card, this.props.tree.cards, this.props.tree.activeCard); 40 | 41 | return ( 42 |
this.props.setEditing(true)} 45 | style={(card.color?{borderLeft: `3px solid ${card.color}`}:null)} 46 | className={"card " 47 | + (active == "card" ? "active " : "") 48 | + (isDragging ? "dragging " : "") 49 | + (active == "children" ? "children-active" : "")}> 50 | {connectDragSource( 51 |
52 | )} 53 | {connectDragPreview(
)} 54 | 55 | 56 | 57 |
this.props.createCard("before", card)}>+
59 |
this.props.createCard("before", card)}> 61 | 62 |
63 | 64 |
this.props.createCard("right", card)}>+
66 |
this.props.createCard("after", card)}>+
68 |
this.props.deleteCard(card)}> 70 | ❌ 71 |
72 | 73 | 74 | 75 | 77 |
78 |
79 | 80 | Id: 81 | {card.id} 82 | 83 | 84 | Parent: 85 | {getParent(card, this.props.tree.cards).id} 86 | 87 |
88 |
89 | ); 90 | } 91 | } 92 | 93 | 94 | /* Connect Drag and Drop props to the component. */ 95 | function collect(connect, monitor) { 96 | return { 97 | connectDragSource: connect.dragSource(), 98 | isDragging: monitor.isDragging() 99 | }; 100 | } 101 | 102 | 103 | function mapStateToProps(state) { 104 | return { tree: state.tree.present }; 105 | } 106 | var card = connect(mapStateToProps, {...cardsActions, setEditing})(Card); 107 | 108 | /* Connect drag and drop to the card */ 109 | export default DragSource('CARD', cardSource, collect)(card); 110 | -------------------------------------------------------------------------------- /client/components/CardGroup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | /* Actions */ 5 | import * as cardsActions from '../actions/cards.actions'; 6 | /* Utils */ 7 | import { getAllParents } from '../utils/cards'; 8 | import { search } from '../utils/cards'; 9 | 10 | class CardGroup extends Component { 11 | render() { 12 | var { activeCard } = this.props.tree; 13 | var isActive = false; 14 | 15 | /* Hide group if all cards filtered out by search */ 16 | var query = this.props.tree.query; 17 | if (query) { 18 | var hasCards = false; 19 | this.props.group.cards.map((c)=>{ 20 | if (search(c, query)) { 21 | hasCards = true; 22 | } 23 | }); 24 | if (!hasCards) { 25 | return
; 26 | } 27 | } 28 | 29 | /* If parent active */ 30 | if (this.props.group.parent.id == activeCard) { 31 | isActive = "parents"; 32 | } 33 | /* If one of group's cards is active */ 34 | this.props.group.cards.map((c)=>{ 35 | if (c.id == activeCard) { 36 | isActive = "group"; 37 | } 38 | }) 39 | var allParents = getAllParents(this.props.group.parent, this.props.tree.cards); 40 | /* This will activate all the child groups of an active card, 41 | because it checks if any of the group's parent's are active, 42 | and activates the group if they are*/ 43 | allParents.map((p)=>{ 44 | if (p.id == activeCard) { 45 | isActive = "parents"; 46 | } 47 | }); 48 | 49 | return ( 50 |
55 | { this.props.renderCards(this.props.group.cards) } 56 |
57 | ); 58 | } 59 | } 60 | 61 | function mapStateToProps(state) { 62 | return { tree: state.tree.present }; 63 | } 64 | export default connect(mapStateToProps, cardsActions)(CardGroup); 65 | -------------------------------------------------------------------------------- /client/components/CardLimit.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { browserHistory } from 'react-router'; 5 | import { Link } from 'react-router'; 6 | 7 | /* Actions */ 8 | import * as cardsActions from '../actions/cards.actions'; 9 | import {setShowModal} from '../actions/preferences.actions'; 10 | 11 | class CardLimit extends Component { 12 | componentDidUpdate(pastProps, pastState) { 13 | /* If user has created a card - check if he's reached the limit.*/ 14 | if (this.props.tree.cards != pastProps.tree.cards 15 | && this.props.tree.saved == false 16 | && this.props.location.pathname != "/trees") { 17 | 18 | /* If unauthenticated user reached a limit - open must login prompt. */ 19 | /* Checking it using localStorage, 20 | otherwise it pops up before user is fetched. */ 21 | if(!localStorage.getItem('token') 22 | && localStorage.getItem('cardsCreated') > 100){ 23 | this.props.setShowModal("mustLogin"); 24 | } 25 | 26 | /* If free user reached a limit - open upgrade prompt. */ 27 | if (this.props.user.plan == "Free" 28 | && localStorage.getItem('cardsCreated') > this.props.user.cardLimit){ 29 | this.props.setShowModal("upgrade"); 30 | } 31 | } 32 | } 33 | 34 | 35 | renderLimitIndicator() { 36 | var cardsCreated = 0; 37 | if (localStorage.getItem('cardsCreated')) { 38 | cardsCreated = localStorage.getItem('cardsCreated'); 39 | } 40 | 41 | if (!localStorage.getItem('token')) { 42 | /* If the user isn't logged in - opens must login prompt. */ 43 | return ( 44 |
this.props.setShowModal("mustLogin")}> 46 |
48 |
49 |
50 | ) 51 | } 52 | if (this.props.user.plan == "Free") { 53 | /* Making sure user is on Free account, opening upgrade prompt.*/ 54 | return ( 55 |
this.props.setShowModal("upgrade")}> 57 |
59 |
60 |
61 | ) 62 | } 63 | 64 | return null; 65 | } 66 | 67 | render() { 68 | return( 69 |
70 | {this.renderLimitIndicator()} 71 |
72 | ); 73 | } 74 | } 75 | 76 | 77 | function mapStateToProps(state) { 78 | return { 79 | tree: state.tree.present, 80 | user: state.profiles.user 81 | }; 82 | } 83 | 84 | export default connect(mapStateToProps, {setShowModal})(CardLimit); 85 | 86 | -------------------------------------------------------------------------------- /client/components/ColorBox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | /* Actions */ 5 | import {setCardColor} from '../actions/cards.actions'; 6 | 7 | class ColorBox extends Component { 8 | renderColors() { 9 | var colors = ["#0079BF", "#EB5A46","#FFAB4A","#61BD4F","#C377E0", ""]; 10 | return colors.reverse().map((c)=>{ 11 | return ( 12 |
this.props.setCardColor(c)} 15 | style={{background: c}} 16 | >
17 | ) 18 | }) 19 | } 20 | render () { 21 | const { authenticated } = this.props; 22 | 23 | return ( 24 |
25 | {this.renderColors()} 26 |
27 | ); 28 | } 29 | } 30 | 31 | 32 | export default connect(null, {setCardColor})(ColorBox); 33 | 34 | -------------------------------------------------------------------------------- /client/components/ComponentBoilerplate.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { browserHistory } from 'react-router'; 5 | import { Link } from 'react-router'; 6 | 7 | /* Actions */ 8 | import * as cardsActions from '../actions/cards.actions'; 9 | 10 | class Name extends Component { 11 | render () { 12 | return ( 13 |
14 |
15 | ); 16 | } 17 | } 18 | 19 | 20 | function mapStateToProps(state) { 21 | return { 22 | tree: state.tree.present, 23 | user: state.profiles.user 24 | }; 25 | } 26 | 27 | export default connect(mapStateToProps, cardsActions)(Name); 28 | 29 | -------------------------------------------------------------------------------- /client/components/DebuggingPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { browserHistory } from 'react-router'; 5 | import { Link } from 'react-router'; 6 | 7 | /* My Components */ 8 | /* import Header from './Header';*/ 9 | 10 | /* Actions */ 11 | import * as cardsActions from '../actions/cards.actions'; 12 | /* Utils */ 13 | /* import { getCard } from '../utils/cards';*/ 14 | 15 | class Name extends Component { 16 | constructor(props){ 17 | super(props); 18 | /* this.showModal = this.showModal.bind(this);*/ 19 | } 20 | 21 | componentWillMount() { 22 | } 23 | 24 | render () { 25 | const { authenticated } = this.props; 26 | 27 | return ( 28 |
29 | {/* New */} 30 | 31 |

Active Card Id:

32 |

33 | {this.props.tree.activeCard} 34 |

35 |

Modified:

36 |

37 | {this.props.tree.modified} 38 |

39 |

Debugging:

40 |

42 |

43 |

State:

44 |
45 | 		    {JSON.stringify(this.props.tree, null, 4)}
46 | 		
47 |
48 | ); 49 | } 50 | } 51 | 52 | 53 | function mapStateToProps(state) { 54 | return { 55 | tree: state.tree.present, 56 | user: state.profiles.user 57 | }; 58 | } 59 | 60 | export default connect(mapStateToProps, cardsActions)(Name); 61 | 62 | -------------------------------------------------------------------------------- /client/components/DropTarget.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { DropTarget } from 'react-dnd'; 5 | 6 | const dropTarget = { 7 | drop(props) { 8 | /* Return the info I'll be ale to access after the card is dropped. */ 9 | return { 10 | parentId: props.card.id, 11 | position:props.position 12 | }; 13 | }, 14 | }; 15 | @DropTarget('CARD', dropTarget, (connect, monitor) => ({ 16 | connectDropTarget: connect.dropTarget(), 17 | isOver: monitor.isOver(), 18 | canDrop: monitor.canDrop() 19 | })) 20 | class Target extends Component { 21 | render() { 22 | const { canDrop, isOver, connectDropTarget } = this.props; 23 | 24 | /* Highlight it */ 25 | var isActive = canDrop && isOver; 26 | 27 | return connectDropTarget( 28 |
32 | 33 |
34 | ); 35 | } 36 | } 37 | 38 | 39 | export default Target; 40 | 41 | -------------------------------------------------------------------------------- /client/components/EditorTextarea.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | /* Actions */ 5 | import { createCard, updateCard, deleteCard, 6 | moveCard, activateCard, setEditing, 7 | updateTree, loadTree } from '../actions/index'; 8 | 9 | /* Vendor components */ 10 | import Textarea from 'react-textarea-autosize'; 11 | 12 | 13 | class Editor extends Component { 14 | componentDidMount(){ 15 | if (this.props.card.id == this.props.tree.activeCard) { 16 | this.editor.focus(); 17 | this.editor.selectionStart = this.props.card.content.length; 18 | this.editor.selectionEnd= this.props.card.content.length; 19 | } 20 | } 21 | 22 | 23 | componentDidUpdate(prevProps){ 24 | if (this.props.card.id == this.props.tree.activeCard && 25 | this.props.tree.activeCard !== prevProps.tree.activeCard) { 26 | /* If this card is active, and the activeCard has just changed 27 | - focus the editor. */ 28 | this.editor.focus(); 29 | if (this.editor.selectionStart == 0 ) { 30 | /* Move cursor to the end. */ 31 | /* Will only happen if I've just changed between cards, 32 | and the cursor is at the very beginning of the textarea*/ 33 | this.editor.selectionStart= this.props.card.content.length; 34 | this.editor.selectionEnd= this.props.card.content.length; 35 | } 36 | } 37 | } 38 | 39 | render() { 40 | const { card } = this.props; 41 | return ( 42 | 50 | ) 51 | } 52 | } 53 | 54 | 55 | 56 | /* Magic connecting component to redux */ 57 | function mapStateToProps(state) { 58 | return { tree: state.tree.present }; 59 | } 60 | /* First argument allows to access state */ 61 | /* Second allows to fire actions */ 62 | export default connect(mapStateToProps, { updateCard })(Editor); 63 | -------------------------------------------------------------------------------- /client/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { connect } from 'react-redux'; 4 | import { Link } from 'react-router'; 5 | import { browserHistory } from 'react-router'; 6 | 7 | /* Vendor */ 8 | import Mousetrap from 'mousetrap'; 9 | import removeMd from 'remove-markdown'; 10 | 11 | 12 | /* Actions */ 13 | import * as cardsActions from '../actions/cards.actions'; 14 | import * as treesActions from '../actions/trees.actions'; 15 | import {fetchUser, logout} from '../actions/profiles.actions'; 16 | import {setShowModal} from '../actions/preferences.actions'; 17 | import { ActionCreators } from 'redux-undo'; 18 | var { undo, redo } = ActionCreators; 19 | 20 | /* My Components */ 21 | /* Modals */ 22 | import ModalLogin from './ModalLogin'; 23 | /* import ModalTreeSettings from './ModalTreeSettings';*/ 24 | import ModalPayments from './ModalPayments'; 25 | import ModalThankYou from './ModalThankYou'; 26 | import ModalFree from './ModalFree'; 27 | import ModalShare from './ModalShare'; 28 | import ModalDesktop from './ModalDesktop'; 29 | import ModalSupport from './ModalSupport'; 30 | import ModalTreeSettings from './ModalTreeSettings'; 31 | /* Menus */ 32 | import MenuTree from './MenuTree'; 33 | import MenuEdit from './MenuEdit'; 34 | import MenuProfile from './MenuProfile'; 35 | import MenuAbout from './MenuAbout'; 36 | 37 | import CardLimit from './CardLimit'; 38 | import Search from './Search'; 39 | import Stats from './Stats'; 40 | 41 | class Header extends Component { 42 | render() { 43 | const atMyTrees = this.props.location.pathname == "/trees"; 44 | const isDesktop = window.__ELECTRON_ENV__ == 'desktop'; 45 | 46 | return ( 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 | 59 | { !atMyTrees ? 60 | 61 | :null} 62 | 63 | 64 | 65 | 66 | {atMyTrees? 67 |

My Trees

68 | : 69 |

{this.props.tree.name}

70 | } 71 | {!atMyTrees 72 | && this.props.user 73 | && this.props.user.email == this.props.tree.author 74 | && this.props.tree.source == "Online" 75 | && this.props.tree.saved ? 76 | 77 | [saved] 78 | 79 | : null 80 | } 81 | 82 |
83 | 84 |
85 | 86 |
87 | {/* */} 88 |
89 |
90 | ); 91 | } 92 | } 93 | 94 | 95 | function mapStateToProps(state) { 96 | return { 97 | tree: state.tree.present, 98 | user: state.profiles.user, 99 | preferences: state.preferences 100 | }; 101 | } 102 | 103 | export default connect(mapStateToProps, {...cardsActions, ...treesActions, 104 | fetchUser, logout, undo, redo, 105 | setShowModal})(Header); 106 | -------------------------------------------------------------------------------- /client/components/Hotkeys.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { ActionCreators } from 'redux-undo'; 5 | var { undo, redo } = ActionCreators; 6 | 7 | /* Vendor components */ 8 | import Mousetrap from 'mousetrap'; 9 | 10 | /* Actions */ 11 | import * as cardsActions from '../actions/cards.actions'; 12 | import * as treesActions from '../actions/trees.actions'; 13 | 14 | class Hotkeys extends Component { 15 | componentDidMount(){ 16 | const {tree} = this.props; 17 | /* 18 | Mousetrap(document.body).bind(['alt+f'], ()=>{ 19 | console.log("yes"); 20 | return false; 21 | }); 22 | */ 23 | /* Editing */ 24 | Mousetrap(document.body).bind(['enter'], ()=>{ 25 | if (!this.props.tree.editing) { 26 | this.props.setEditing(true); 27 | } 28 | return false; 29 | }); 30 | Mousetrap(document.body).bind(['esc'], ()=>{ 31 | this.props.setEditing(false); 32 | if (tree.saved == false 33 | && tree.source == "Online" 34 | && tree.author == user.email) { 35 | this.props.updateTree(this.props.tree); 36 | } 37 | return false; 38 | }); 39 | 40 | Mousetrap(document.body).bind(['ctrl+enter'], ()=>{ 41 | console.log("Editing mode!"); 42 | var editingNow = this.props.tree.editing; 43 | this.props.setEditing(!this.props.tree.editing); 44 | if (editingNow) { 45 | /* If I've been editing - save it. */ 46 | if (tree.saved == false 47 | && tree.source == "Online" 48 | && tree.author == user.email) { 49 | this.props.updateTree(this.props.tree); 50 | } 51 | } 52 | return false; 53 | }); 54 | 55 | /* Select */ 56 | Mousetrap(document.body).bind(['ctrl+j', 'ctrl+down'], ()=>{ 57 | this.props.selectCard('down'); 58 | return false; 59 | }); 60 | 61 | Mousetrap(document.body).bind(['ctrl+k','ctrl+up'], ()=>{ 62 | this.props.selectCard('up'); 63 | return false; 64 | }); 65 | Mousetrap(document.body).bind(['ctrl+h', 'ctrl+left'], ()=>{ 66 | this.props.selectCard('left'); 67 | return false; 68 | }); 69 | Mousetrap(document.body).bind(['ctrl+l', 'ctrl+right'], ()=>{ 70 | this.props.selectCard('right'); 71 | return false; 72 | }); 73 | /* Create */ 74 | Mousetrap(document.body).bind(['ctrl+shift+l', 'ctrl+shift+right'], ()=>{ 75 | this.props.createCard("right"); 76 | return false; 77 | }); 78 | Mousetrap(document.body).bind(['ctrl+shift+k', 'ctrl+shift+up'], ()=>{ 79 | this.props.createCard("before"); 80 | return false; 81 | }); 82 | Mousetrap(document.body).bind(['ctrl+shift+j', 'ctrl+shift+down'], ()=>{ 83 | this.props.createCard("after"); 84 | return false; 85 | }); 86 | /* Move */ 87 | Mousetrap(document.body).bind(['alt+j', 'alt+down'], ()=>{ 88 | this.props.moveCard("down"); 89 | return false; 90 | }); 91 | Mousetrap(document.body).bind(['alt+k', 'alt+up'], ()=>{ 92 | this.props.moveCard("up"); 93 | return false; 94 | }); 95 | Mousetrap(document.body).bind(['alt+l', 'alt+right'], ()=>{ 96 | this.props.moveCard("right"); 97 | return false; 98 | }); 99 | Mousetrap(document.body).bind(['alt+h', 'alt+left'], ()=>{ 100 | this.props.moveCard("left"); 101 | return false; 102 | }); 103 | /* Edit */ 104 | /* 105 | Mousetrap(document.body).bind(['enter'], ()=>{ 106 | this.props.setEditing(true); 107 | return false; 108 | }); 109 | */ 110 | /* Save */ 111 | /* 112 | Mousetrap(document.body).bind(['ctrl+s'], ()=>{ 113 | this.props.updateTree(this.props.tree.present); 114 | return false; 115 | }); 116 | */ 117 | 118 | /* Delete */ 119 | Mousetrap(document.body).bind(['ctrl+backspace'], ()=>{ 120 | this.props.deleteCard(); 121 | return false; 122 | }); 123 | 124 | /* Undo/Redo */ 125 | Mousetrap(document.body).bind(['ctrl+z'], ()=>{ 126 | this.props.undo(); 127 | return false; 128 | }); 129 | Mousetrap(document.body).bind(['ctrl+shift+z'], ()=>{ 130 | this.props.redo(); 131 | return false; 132 | }); 133 | 134 | /* Edit card color */ 135 | Mousetrap(document.body).bind(['alt+c'], ()=>{ 136 | this.props.setCardConfig(!this.props.tree.showCardConfig); 137 | return false; 138 | }); 139 | 140 | /* Unbind */ 141 | /* Mousetrap.unbind('tab.form-control');*/ 142 | 143 | 144 | } 145 | 146 | componentWillUnmount() { 147 | /* unbindHotkeys();*/ 148 | } 149 | 150 | render() { 151 | return null; 152 | } 153 | } 154 | 155 | 156 | function mapStateToProps(state) { 157 | return { tree: state.tree.present }; 158 | } 159 | 160 | export default connect(mapStateToProps, {...cardsActions, ...treesActions, 161 | undo, redo})(Hotkeys); 162 | 163 | -------------------------------------------------------------------------------- /client/components/MenuAbout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { browserHistory } from 'react-router'; 5 | import { Link } from 'react-router'; 6 | 7 | /* My Components */ 8 | /* import Header from './Header';*/ 9 | 10 | /* Actions */ 11 | import * as treesActions from '../actions/trees.actions'; 12 | import {setShowModal} from '../actions/preferences.actions'; 13 | 14 | class MenuAbout extends Component { 15 | render () { 16 | const isDesktop = window.__ELECTRON_ENV__ == 'desktop'; 17 | 18 | return ( 19 | 20 | About 21 | 57 | 58 | ); 59 | } 60 | } 61 | 62 | 63 | function mapStateToProps(state) { 64 | return null; 65 | } 66 | 67 | export default connect(null, {...treesActions, setShowModal})(MenuAbout); 68 | 69 | -------------------------------------------------------------------------------- /client/components/MenuEdit.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { browserHistory } from 'react-router'; 5 | import { Link } from 'react-router'; 6 | 7 | /* My Components */ 8 | /* import Header from './Header';*/ 9 | 10 | /* Actions */ 11 | import {deleteCard} from '../actions/cards.actions'; 12 | import {setShowModal} from '../actions/preferences.actions'; 13 | 14 | import { ActionCreators } from 'redux-undo'; 15 | var { undo, redo } = ActionCreators; 16 | 17 | class MenuEdit extends Component { 18 | render () { 19 | return ( 20 | 21 | Edit 22 | 66 | 67 | ); 68 | } 69 | } 70 | 71 | 72 | function mapStateToProps(state) { 73 | return { 74 | tree: state.tree.present, 75 | user: state.profiles.user 76 | }; 77 | } 78 | 79 | export default connect(mapStateToProps, {deleteCard, undo, redo, setShowModal})(MenuEdit); 80 | 81 | -------------------------------------------------------------------------------- /client/components/MenuProfile.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { browserHistory } from 'react-router'; 5 | import { Link } from 'react-router'; 6 | 7 | /* Actions */ 8 | import * as profilesActions from '../actions/profiles.actions'; 9 | import {setShowModal} from '../actions/preferences.actions'; 10 | 11 | class MenuProfile extends Component { 12 | renderUserMenu() { 13 | return( 14 | 33 | ); 34 | } 35 | 36 | renderLoginMenu(){ 37 | return( 38 | 50 | ); 51 | } 52 | render () { 53 | return ( 54 | 55 | Profile 56 | { this.props.user ? 57 | this.renderUserMenu() 58 | : 59 | this.renderLoginMenu() 60 | } 61 | 62 | ); 63 | } 64 | } 65 | 66 | 67 | function mapStateToProps(state) { 68 | return { 69 | tree: state.tree.present, 70 | user: state.profiles.user 71 | }; 72 | } 73 | 74 | export default connect(mapStateToProps, {...profilesActions, setShowModal})(MenuProfile); 75 | 76 | -------------------------------------------------------------------------------- /client/components/MetaInfo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | /* Vendor Components */ 5 | import MetaTags from 'react-meta-tags'; 6 | import removeMd from 'remove-markdown'; 7 | 8 | class MetaInfo extends Component { 9 | render () { 10 | const { tree } = this.props; 11 | 12 | var title = "Nulis"; 13 | if (tree.name) { 14 | title = tree.name + " - Nulis"; 15 | } 16 | const description = removeMd(tree.cards.children[0].content).substring(0,120); 17 | 18 | return ( 19 | 20 | {/* Main */} 21 | {title} 22 | 24 | {/* Facebook */} 25 | 26 | 27 | {/* Twitter */} 28 | 29 | 30 | ); 31 | } 32 | } 33 | 34 | 35 | function mapStateToProps(state) { 36 | return { 37 | tree: state.tree.present 38 | }; 39 | } 40 | 41 | export default connect(mapStateToProps, {})(MetaInfo); 42 | 43 | -------------------------------------------------------------------------------- /client/components/ModalDesktop.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | /* Vendor Components */ 6 | import { Modal } from 'react-bootstrap'; 7 | 8 | /* Actions */ 9 | import {setShowModal} from '../actions/preferences.actions'; 10 | 11 | class ModalFree extends Component { 12 | render() { 13 | return ( 14 | this.props.setShowModal(false)}> 17 | 18 |

Download Nulis Desktop

19 |
20 |
21 |

Desktop version of Nulis is now available for 22 | all the platforms!
23 | Awesome, right? =)

24 |
25 | Download 27 |

Linux

28 |
29 |
30 | 31 |
32 | Download 34 |

Mac

35 |

(Untested. Message me to report bugs.)

36 |
37 |
38 | 39 |
40 | Download 42 |

Windows

43 |

(Untested. Message me to report bugs.)

44 |
45 |
46 | 47 | 48 |
49 |
50 |
51 | ); 52 | } 53 | } 54 | 55 | 56 | function mapStateToProps(state) { 57 | return { 58 | showModal: state.preferences.showModal 59 | }; 60 | } 61 | 62 | export default connect(mapStateToProps, { setShowModal })(ModalFree); 63 | 64 | -------------------------------------------------------------------------------- /client/components/ModalFree.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | /* Vendor Components */ 6 | import { Modal } from 'react-bootstrap'; 7 | 8 | /* Actions */ 9 | import {setShowModal} from '../actions/preferences.actions'; 10 | 11 | class ModalFree extends Component { 12 | render() { 13 | return ( 14 | this.props.setShowModal(false)}> 17 | 18 |

Surprise free upgrade!

19 |
20 |
21 |

Thank you very much for deciding to purchase Nulis!

22 |

I have not implemented the payments yet, but I want to thank you for being one of the early users, so the upgrade is yours completely free! =)

23 |

If you would still like to support this project - you can share it with your friends, or send me an email to raymestalez@gmail.com with some feedback or feature requests. That would really help me to make Nulis more awesome!

24 |
25 |
26 | ); 27 | } 28 | } 29 | 30 | 31 | function mapStateToProps(state) { 32 | return { 33 | showModal: state.preferences.showModal 34 | }; 35 | } 36 | 37 | export default connect(mapStateToProps, { setShowModal })(ModalFree); 38 | 39 | -------------------------------------------------------------------------------- /client/components/ModalLogin.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | /* Vendor Components */ 6 | import { Modal } from 'react-bootstrap'; 7 | import Cookies from "js-cookie"; 8 | 9 | /* Actions */ 10 | import {login,join} from '../actions/profiles.actions'; 11 | import {setShowModal} from '../actions/preferences.actions'; 12 | 13 | class ModalLogin extends Component { 14 | /* Note: if it's mustLogin, then I create account. */ 15 | onSubmit(event) { 16 | event.preventDefault(); 17 | const credentials = { 18 | email: ReactDOM.findDOMNode(this.refs.email).value, 19 | password: ReactDOM.findDOMNode(this.refs.password).value 20 | }; 21 | 22 | const type = this.props.showModal; 23 | if (type == "login") { 24 | this.props.login(credentials); 25 | } else { 26 | /* If I'm creating a new account */ 27 | /* Passing the cookies I've set in App.js */ 28 | credentials.referral = Cookies.get('referral'); 29 | credentials.source = Cookies.get('source'); 30 | this.props.join(credentials); 31 | } 32 | this.props.setShowModal(false); 33 | } 34 | 35 | render() { 36 | const type = this.props.showModal; 37 | return ( 38 | this.props.setShowModal(false)}> 41 |
42 | {this.props.error? 43 |
44 |
45 | {this.props.error} 46 |
47 |
48 | :null} 49 |
50 | { type=="login" ? 51 |

Login

52 | : 53 |

Create Account

54 | } 55 |
56 | {type=="mustLogin" ? 57 |
58 |

Unregistered users can create up to 100 cards.
59 | To get more cards, create an account!

60 |
61 | :null} 62 |
63 | 64 | 65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 |
73 | { type=="login" ? 74 | 76 | : 77 | 79 | } 80 |
81 | 82 |
83 |
84 | ); 85 | } 86 | } 87 | 88 | 89 | function mapStateToProps(state) { 90 | return { 91 | user: state.profiles.user, 92 | error: state.profiles.error, 93 | showModal: state.preferences.showModal 94 | }; 95 | } 96 | 97 | export default connect(mapStateToProps, {login, join, setShowModal})(ModalLogin); 98 | 99 | -------------------------------------------------------------------------------- /client/components/ModalShare.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | /* Vendor Components */ 6 | import { Modal } from 'react-bootstrap'; 7 | 8 | /* Actions */ 9 | import {setShowModal} from '../actions/preferences.actions'; 10 | 11 | 12 | class ModalShare extends Component { 13 | render() { 14 | return ( 15 | this.props.setShowModal(false)}> 18 | 19 |

Get Nulis for Free!

20 |
21 |
22 |

There are two ways to get the unlimited version of Nulis for free.

23 |

1. Share this link with your friends:

24 |
25 | https://nulis.io/?ref={this.props.referralCode} 26 |
27 |

You will get extra 100 cards for every person who signs up using this link, and they get 100 extra cards as well.
If you will invite 10 people - you get a lifetime unlimited account for free.

28 |

2. Write a blog post about Nulis, send a link to raymestalez@gmail.com - and I will give you an unlimited account.

29 | 30 | {this.props.setShowModal("upgrade");}}> 32 | < Back to Plans 33 | 34 | 35 |
36 |
37 | ); 38 | } 39 | } 40 | 41 | 42 | function mapStateToProps(state) { 43 | return { 44 | showModal: state.preferences.showModal, 45 | referralCode: state.profiles.user.referralCode 46 | }; 47 | } 48 | 49 | export default connect(mapStateToProps, { setShowModal })(ModalShare); 50 | 51 | -------------------------------------------------------------------------------- /client/components/ModalSupport.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | /* Vendor Components */ 6 | import { Modal } from 'react-bootstrap'; 7 | 8 | /* Actions */ 9 | import {setShowModal} from '../actions/preferences.actions'; 10 | 11 | class ModalFree extends Component { 12 | render() { 13 | return ( 14 | this.props.setShowModal(false)}> 17 | 18 |

Support

19 |
20 |
21 |

Hey there! If you have any questions, feedback, or bug reports, feel free to send me an email to raymestalez@gmail.com. 22 |

23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | 30 | function mapStateToProps(state) { 31 | return { 32 | showModal: state.preferences.showModal 33 | }; 34 | } 35 | 36 | export default connect(mapStateToProps, { setShowModal })(ModalFree); 37 | 38 | -------------------------------------------------------------------------------- /client/components/ModalThankYou.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | /* Vendor Components */ 6 | import { Modal } from 'react-bootstrap'; 7 | 8 | /* Actions */ 9 | import {setShowModal} from '../actions/preferences.actions'; 10 | 11 | 12 | class ModalFree extends Component { 13 | render() { 14 | return ( 15 | this.props.setShowModal(false)}> 18 | 19 |

Thank you!

20 |
21 |
22 |

Thank you so much for your support! =) 23 | I really hope that Nulis will be very useful to you.

24 |

If you have any questions, issues, or would like to 25 | suggest a feature - always feel free to send me an email to 26 | raymestalez@gmail.com

27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | 34 | function mapStateToProps(state) { 35 | return { 36 | showModal: state.preferences.showModal 37 | }; 38 | } 39 | 40 | export default connect(mapStateToProps, { setShowModal })(ModalFree); 41 | 42 | -------------------------------------------------------------------------------- /client/components/ModalTreeSettings.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | /* Vendor Components */ 7 | import { Modal } from 'react-bootstrap'; 8 | 9 | /* Actions */ 10 | import * as treesActions from '../actions/trees.actions'; 11 | import {setShowModal} from '../actions/preferences.actions'; 12 | 13 | class ModalTreeSettings extends Component { 14 | onSubmit(event) { 15 | event.preventDefault(); 16 | var name = ReactDOM.findDOMNode(this.refs.name).value; 17 | var tree = this.props.tree; 18 | tree.name = name; 19 | 20 | this.props.updateTree(tree); 21 | this.props.setShowModal(false); 22 | } 23 | 24 | render () { 25 | return ( 26 | this.props.setShowModal(false)}> 29 | 30 |

Tree Settings

31 |
32 |
33 |
34 | 35 | 39 | 40 |
41 | 45 |
46 | 52 |
53 |
54 |
55 | ) 56 | } 57 | } 58 | 59 | 60 | 61 | function mapStateToProps(state) { 62 | return { 63 | tree: state.tree.present, 64 | showModal: state.preferences.showModal 65 | }; 66 | } 67 | 68 | export default connect(mapStateToProps, {...treesActions, setShowModal})(ModalTreeSettings); 69 | 70 | -------------------------------------------------------------------------------- /client/components/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | import { browserHistory } from 'react-router'; 6 | import { Link } from 'react-router'; 7 | 8 | /* My Components */ 9 | 10 | /* Actions */ 11 | import * as treesActions from '../actions/trees.actions'; 12 | 13 | class Search extends Component { 14 | componentDidMount(){ 15 | /* Shortcuts */ 16 | /* Focus on search */ 17 | Mousetrap(document.body).bind(['ctrl+/'], ()=>{ 18 | ReactDOM.findDOMNode(this.refs.search).focus(); 19 | return false; 20 | }); 21 | } 22 | 23 | render () { 24 | return ( 25 | 26 | 29 | this.props.updateSearchQuery(event.target.value)}/> 30 | 31 | 32 | ); 33 | } 34 | } 35 | 36 | function mapStateToProps(state) { 37 | return { 38 | query: state.tree.present.query 39 | }; 40 | } 41 | 42 | export default connect(mapStateToProps, treesActions)(Search); 43 | 44 | -------------------------------------------------------------------------------- /client/components/Stats.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { browserHistory } from 'react-router'; 5 | import { Link } from 'react-router'; 6 | 7 | /* Vendor Components */ 8 | import { Modal } from 'react-bootstrap'; 9 | 10 | /* Actions */ 11 | import * as cardsActions from '../actions/cards.actions'; 12 | import {setShowModal} from '../actions/preferences.actions'; 13 | 14 | class Stats extends Component { 15 | renderCalendar() { 16 | return( 17 |
18 |
19 | {this.props.user.stats.streak} 20 |
21 |
22 |
23 | {this.props.user.stats.calendar.map((day)=> { 24 | var brightness = 0; 25 | if (day.wordcount > 100){ 26 | brightness = (day.wordcount+100)/1000; 27 | } 28 | return
31 | })} 32 |
33 |
34 |
35 | ); 36 | } 37 | 38 | renderWordcounter() { 39 | var calendar = this.props.user.stats.calendar 40 | var wordcount = calendar[calendar.length - 1].wordcount; 41 | 42 | return( 43 |
44 |
45 | {wordcount} 46 |
47 |
48 |
50 |
51 |
52 | {[...Array(10).keys()].map((i)=> { 53 | return
54 | })} 55 |
56 |
57 |
58 | ); 59 | } 60 | render() { 61 | if (!this.props.user ) { return null; } 62 | return( 63 |
this.props.setShowModal("stats")}> 65 | {this.renderCalendar()} 66 | {this.renderWordcounter()} 67 | this.props.setShowModal(false)}> 70 | 71 |

Stats

72 |
73 |
74 |

This is your writing stats.

75 |

The top bar is the calendar of the last 10 days, 76 | brightness of the day depends on the amount of words you have 77 | written(0 - completely white, 1000 - completely orange).

78 |

Number to the left of it is your current streak - how many days in a row you have written at least 100 words.

79 |

The bottom bar represents the number of words you wrote 80 | today.

81 |
82 |
83 | 84 |
85 | ); 86 | } 87 | } 88 | 89 | 90 | function mapStateToProps(state) { 91 | return { 92 | user: state.profiles.user, 93 | showModal: state.preferences.showModal 94 | }; 95 | } 96 | 97 | export default connect(mapStateToProps, {setShowModal})(Stats); 98 | 99 | -------------------------------------------------------------------------------- /client/components/TreeManager.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | import { browserHistory } from 'react-router'; 5 | 6 | /* Actions */ 7 | import * as treesActions from '../actions/trees.actions'; 8 | 9 | /* Utils */ 10 | import handleScroll from '../utils/handleScroll'; 11 | 12 | class TreeManager extends Component { 13 | componentDidMount(){ 14 | this.props.listTrees(); 15 | } 16 | 17 | renderTreeList() { 18 | var { allTrees } = this.props; 19 | if (!allTrees) { return
} 20 | 21 | var treeList = allTrees.map((t)=>{ 22 | var capitalized = t.name.charAt(0).toUpperCase() + t.name.slice(1); 23 | return ( 24 |
25 | 26 | {capitalized} 27 | 28 | this.props.deleteTree(t)}> 30 | 31 | 32 | 33 |
34 | ); 35 | }); 36 | var newTree = [ 37 |
38 | 39 | 40 | New Tree 41 | 42 |
43 | ] 44 | return newTree.concat(treeList) 45 | } 46 | render() { 47 | return ( 48 |
49 | {this.renderTreeList()} 50 |
51 | ); 52 | } 53 | } 54 | 55 | function mapStateToProps(state) { 56 | return { 57 | allTrees: state.allTrees 58 | }; 59 | } 60 | 61 | export default connect(mapStateToProps, treesActions)(TreeManager); 62 | 63 | -------------------------------------------------------------------------------- /client/components/auth/login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/profiles'; 4 | import ReactDOM from 'react-dom'; 5 | import { Link } from 'react-router'; 6 | 7 | class Login extends Component { 8 | onSubmit(event) { 9 | event.preventDefault(); 10 | const credentials = { 11 | email: ReactDOM.findDOMNode(this.refs.email).value, 12 | password: ReactDOM.findDOMNode(this.refs.password).value 13 | }; 14 | console.log("Credentials " + JSON.stringify(credentials)); 15 | 16 | const { path } = this.props.route; 17 | if (path == "login") { 18 | this.props.login(credentials); 19 | } else { 20 | this.props.join(credentials); 21 | } 22 | } 23 | 24 | render () { 25 | const { path } = this.props.route; 26 | return ( 27 |
28 | {this.props.errorMessage? 29 |
30 |
31 | {this.props.errorMessage} 32 |
33 |
34 | :null} 35 |
36 | { path=="login"? 37 |

Login

38 | : 39 |

Create Account

40 | } 41 |
42 |
43 | 44 | 45 |
46 |
47 | 48 | 49 |
50 |
51 |
52 | { path=="login" ? 53 | Don't have an account? Join here. 54 | : 55 | Have an account? Login here. 56 | } 57 | 58 |
59 |
60 | ); 61 | } 62 | } 63 | 64 | 65 | function mapStateToProps(state) { 66 | return {}; 67 | } 68 | 69 | export default connect(mapStateToProps, actions)(Login); 70 | 71 | -------------------------------------------------------------------------------- /client/components/auth/require_auth.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | export default function(ComposedComponent) { 5 | class Authentication extends Component { 6 | static contextTypes = { 7 | router: React.PropTypes.object 8 | } 9 | 10 | componentWillMount() { 11 | if (!this.props.authenticated) { 12 | this.context.router.push('/'); 13 | } 14 | } 15 | 16 | componentWillUpdate(nextProps) { 17 | if (!nextProps.authenticated) { 18 | this.context.router.push('/'); 19 | } 20 | } 21 | 22 | render() { 23 | return 24 | } 25 | } 26 | 27 | function mapStateToProps(state) { 28 | return { authenticated: state.auth.authenticated }; 29 | } 30 | 31 | return connect(mapStateToProps)(Authentication); 32 | } 33 | -------------------------------------------------------------------------------- /client/components/auth/signin.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../actions/profiles'; 4 | import { Link } from 'react-router'; 5 | 6 | class Login extends Component { 7 | onSubmit(event) { 8 | event.preventDefault(); 9 | console.log('Email: ' + ReactDOM.findDOMNode(this.refs.email).value); 10 | const email = {email: ReactDOM.findDOMNode(this.refs.email).value}; 11 | this.props.createSubscriber(email); 12 | 13 | console.log("Credentials " + JSON.stringify(credentials)); 14 | 15 | this.props.loginUser({email,password}); 16 | } 17 | 18 | renderAlert(){ 19 | if (this.props.errorMessage) { 20 | return ( 21 |
22 | {this.props.errorMessage} 23 |
24 | ); 25 | } 26 | } 27 | render () { 28 | /* props from reduxForm */ 29 | const { handleSubmit, fields: { email, password }} = this.props; 30 | 31 | /* console.log(...email);*/ 32 | return ( 33 |
34 |
35 |

Login

36 |
37 |
38 | {this.renderAlert()} 39 |
40 |
41 | 42 | 43 |
44 |
45 | 46 | 47 |
48 |
49 |
50 | Create new account. 51 | 52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | 59 | function mapStateToProps(state) { 60 | return {}; 61 | } 62 | 63 | export default connect(mapStateToProps, actions)(Login); 64 | 65 | -------------------------------------------------------------------------------- /client/components/auth/signout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../actions/profiles'; 4 | 5 | class Signout extends Component { 6 | componentWillMount(){ 7 | // as soon as it renders - login user out 8 | console.log(">>>> src/components/auth/signout.js:"); 9 | console.log("Calling signoutUser action creator."); 10 | this.props.signoutUser(); 11 | } 12 | render(){ 13 | return ( 14 |
Signed out!
15 | ); 16 | } 17 | } 18 | 19 | export default connect(null, actions)(Signout); 20 | -------------------------------------------------------------------------------- /client/components/auth/signup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm } from 'redux-form'; 4 | import * as actions from '../../actions/profiles'; 5 | import { Link } from 'react-router'; 6 | 7 | class Signup extends Component { 8 | handleFormSubmit({email, password}) { 9 | /* console.log(email, password);*/ 10 | // signupUser comes from actions. 11 | // it is an action creator that sends an email/pass to the server 12 | // and if they're correct, saves the token 13 | var credentials = { 14 | "email": email, 15 | "password": password 16 | } 17 | console.log("Credentials " + JSON.stringify(credentials)); 18 | 19 | this.props.signupUser({email,password}); 20 | } 21 | 22 | renderAlert(){ 23 | if (this.props.errorMessage) { 24 | return ( 25 |
26 | {this.props.errorMessage} 27 |
28 | ); 29 | } 30 | } 31 | render () { 32 | /* props from reduxForm */ 33 | const { handleSubmit, fields: { email, password, passwordConfirm }} = this.props; 34 | /* console.log(...email);*/ 35 | console.log(this.props.fields); 36 | 37 | 38 | return ( 39 |
40 |
41 |

Create an account

42 |
43 |
44 | {this.renderAlert()} 45 |
46 |
47 | 48 | 49 |
50 |
51 | 52 | 53 |
54 |
55 |
56 | Have an account? Login here. 57 | 58 |
59 |
60 | ); 61 | } 62 | } 63 | 64 | function mapStateToProps(state) { 65 | return { errorMessage:state.auth.error }; 66 | } 67 | 68 | /* 69 | function validate(formProps) { 70 | const errors = {}; 71 | 72 | if (!formProps.email) { 73 | errors.email = "Enter an email"; 74 | } 75 | 76 | if (!formProps.password) { 77 | errors.password = "Enter a password"; 78 | } 79 | 80 | return errors; 81 | } 82 | */ 83 | 84 | export default connect(mapStateToProps, actions)( 85 | reduxForm({ 86 | form: 'signup', 87 | fields: ['email','password'], 88 | }) 89 | (Signup) 90 | ); 91 | /* validate: validate */ 92 | -------------------------------------------------------------------------------- /client/data.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TREE = { 2 | name: "", 3 | cards: { 4 | id:"root", 5 | children: [ 6 | { 7 | id: "0", 8 | content: "Default tree.", 9 | children: [] 10 | }, 11 | ] 12 | }, 13 | activeCard: "0", 14 | modified: false, 15 | editing: false, 16 | query: "" 17 | } 18 | -------------------------------------------------------------------------------- /client/dist/674f50d287a8c48dc19ba404d20fe713.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/dist/674f50d287a8c48dc19ba404d20fe713.eot -------------------------------------------------------------------------------- /client/dist/b06871f281fee6b241d60582ae9369b9.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/dist/b06871f281fee6b241d60582ae9369b9.ttf -------------------------------------------------------------------------------- /client/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nulis 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nulis - a tree editor for writers 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { Router, Route, browserHistory } from 'react-router'; 5 | import { createStore, applyMiddleware } from 'redux'; 6 | import reduxThunk from 'redux-thunk'; 7 | 8 | import routes from './routes'; 9 | import reducers from './reducers'; 10 | 11 | // Connect reduxThunk to middleware so I could dispatch async actions with axios. 12 | const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore); 13 | 14 | // store contains the state 15 | const store = createStoreWithMiddleware(reducers); 16 | 17 | /* Google analytics */ 18 | import settings from '../config/settings.js'; 19 | import ReactGA from "react-ga"; 20 | ReactGA.initialize(settings.googleAnalyticsCode); 21 | function logPageView() { 22 | ReactGA.set({ page: window.location.pathname }); 23 | ReactGA.pageview(window.location.pathname); 24 | window.scrollTo(0, 0); 25 | } 26 | 27 | 28 | ReactDOM.render( 29 | 30 | 31 | 32 | , document.querySelector('.app')); 33 | 34 | -------------------------------------------------------------------------------- /client/media/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/favicon.png -------------------------------------------------------------------------------- /client/media/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/header.jpg -------------------------------------------------------------------------------- /client/media/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/header.png -------------------------------------------------------------------------------- /client/media/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/images/logo.png -------------------------------------------------------------------------------- /client/media/logo_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/logo_128x128.png -------------------------------------------------------------------------------- /client/media/logo_dm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/logo_dm.png -------------------------------------------------------------------------------- /client/media/nulis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/nulis.png -------------------------------------------------------------------------------- /client/media/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/screenshot-1.png -------------------------------------------------------------------------------- /client/media/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/screenshot-3.png -------------------------------------------------------------------------------- /client/media/social-leaderboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/social-leaderboard.png -------------------------------------------------------------------------------- /client/media/social-prompts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/social-prompts.png -------------------------------------------------------------------------------- /client/media/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/media/social.png -------------------------------------------------------------------------------- /client/reducers/#profiles.reducer.js#: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | 4 | function calculateStreak(calendar) { 5 | var currentStreak = 0; 6 | /* Order days starting with the most recent one */ 7 | var days = [...calendar]; 8 | days.reverse(); 9 | /* console.log("days " + JSON.stringify(days));*/ 10 | /* Loop through days */ 11 | for (var i = 0; i < days.length; i++) { 12 | var d = days[i]; 13 | if (d.wordcount > 99) { 14 | /* Increment the streak if this habit is completed */ 15 | currentStreak += 1; 16 | } else { 17 | break; 18 | }; 19 | }; 20 | console.log("Setting state currentStreak " + currentStreak); 21 | return currentStreak; 22 | } 23 | 24 | 25 | function generateCalendar(savedWordcounts) { 26 | /* This function generates a calendar for the past 10 days, 27 | and inserts the information from saved wordcounts into it */ 28 | var today = moment(); 29 | var days = []; 30 | 31 | /* Loop through the past several days */ 32 | for (var i = 0; i < 10; i++) { 33 | var date = today.format('YYYY-MM-DD'); 34 | /* Default wordcount */ 35 | var wordcount = 0; 36 | 37 | /* Search through all the saved wordcounts */ 38 | savedWordcounts.map((c)=>{ 39 | /* If I found a saved wordcount for today */ 40 | if (c.date === date) { 41 | /* I set it's wordcount */ 42 | wordcount = c.wordcount; 43 | } 44 | }); 45 | 46 | /* Add day to the list */ 47 | days.push({ 48 | date: date, 49 | wordcount: wordcount, 50 | }) 51 | /* Previous day */ 52 | today.subtract(1,'days'); 53 | } 54 | console.log(days); 55 | /* Return wordcounts in reverse order */ 56 | return days.reverse(); 57 | } 58 | 59 | 60 | 61 | 62 | var INITIAL_STATE = { 63 | user: "", 64 | error: "" 65 | } 66 | export default function(state=INITIAL_STATE, action) { 67 | switch(action.type) { 68 | case 'AUTH_USER': 69 | var user = action.payload; 70 | console.log(user); 71 | var fullCalendar = generateCalendar(user.stats.calendar); 72 | user.stats.calendar = fullCalendar; 73 | user.stats.streak = calculateStreak(fullCalendar); 74 | return {...state, user: user, error: "" }; 75 | case 'UNAUTH_USER': 76 | return {...state, user: "", error: "" }; 77 | case 'AUTH_ERROR': 78 | return {...state, error: action.payload }; 79 | case 'ADD_WORD': 80 | var calendar = state.user.stats.calendar; 81 | calendar = [...calendar]; 82 | var today = calendar[calendar.length - 1]; 83 | today.wordcount += 1; 84 | 85 | return {...state, user: { 86 | ...state.user, 87 | stats:{ 88 | ...state.user.stats, 89 | calendar: calendar, 90 | streak: calculateStreak(calendar) 91 | }}}; 92 | 93 | } 94 | 95 | return state; 96 | } 97 | -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import undoable from 'redux-undo'; 3 | 4 | /* My reducers */ 5 | import TreeReducer from './tree.reducer'; 6 | import AllTreesReducer from './trees.reducer'; 7 | import ProfilesReducer from './profiles.reducer'; 8 | import PreferencesReducer from './preferences.reducer'; 9 | 10 | const rootReducer = combineReducers({ 11 | tree: undoable(TreeReducer, { limit: 32 }), 12 | allTrees: AllTreesReducer, 13 | profiles: ProfilesReducer, 14 | preferences: PreferencesReducer 15 | }); 16 | 17 | export default rootReducer; 18 | 19 | -------------------------------------------------------------------------------- /client/reducers/preferences.reducer.js: -------------------------------------------------------------------------------- 1 | 2 | var INITIAL_STATE = { 3 | maxColumns: 5, 4 | theme: "Light", 5 | showModal: "" 6 | } 7 | 8 | export default function(state=INITIAL_STATE, action) { 9 | switch(action.type) { 10 | case 'SET_MAX_COLUMNS': 11 | return {...state, maxColumns: action.payload}; 12 | case 'SET_SHOW_MODAL': 13 | console.log("Show modal " + action.payload); 14 | return {...state, showModal: action.payload}; 15 | default: 16 | return state; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/reducers/profiles.reducer.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | 4 | function calculateStreak(calendar) { 5 | var currentStreak = 0; 6 | /* Order days starting with the most recent one */ 7 | var days = [...calendar]; 8 | days.reverse(); 9 | /* console.log("days " + JSON.stringify(days));*/ 10 | /* Loop through days */ 11 | for (var i = 0; i < days.length; i++) { 12 | var d = days[i]; 13 | if (d.wordcount > 99) { 14 | /* Increment the streak if this habit is completed */ 15 | currentStreak += 1; 16 | } else { 17 | break; 18 | }; 19 | }; 20 | console.log("Setting state currentStreak " + currentStreak); 21 | return currentStreak; 22 | } 23 | 24 | 25 | function generateCalendar(savedWordcounts) { 26 | /* This function generates a calendar for the past 10 days, 27 | and inserts the information from saved wordcounts into it */ 28 | var today = moment(); 29 | var days = []; 30 | 31 | /* Loop through the past several days */ 32 | for (var i = 0; i < 10; i++) { 33 | var date = today.format('YYYY-MM-DD'); 34 | /* Default wordcount */ 35 | var wordcount = 0; 36 | 37 | /* Search through all the saved wordcounts */ 38 | savedWordcounts.map((c)=>{ 39 | /* If I found a saved wordcount for today */ 40 | if (c.date === date) { 41 | /* I set it's wordcount */ 42 | wordcount = c.wordcount; 43 | } 44 | }); 45 | 46 | /* Add day to the list */ 47 | days.push({ 48 | date: date, 49 | wordcount: wordcount, 50 | }) 51 | /* Previous day */ 52 | today.subtract(1,'days'); 53 | } 54 | console.log(days); 55 | /* Return wordcounts in reverse order */ 56 | return days.reverse(); 57 | } 58 | 59 | 60 | 61 | 62 | var INITIAL_STATE = { 63 | user: "", 64 | error: "" 65 | } 66 | export default function(state=INITIAL_STATE, action) { 67 | switch(action.type) { 68 | case 'AUTH_USER': 69 | var user = action.payload; 70 | console.log(user); 71 | var fullCalendar = generateCalendar(user.stats.calendar); 72 | user.stats.calendar = fullCalendar; 73 | user.stats.streak = calculateStreak(fullCalendar); 74 | return {...state, user: user, error: "" }; 75 | case 'UNAUTH_USER': 76 | return {...state, user: "", error: "" }; 77 | case 'AUTH_ERROR': 78 | return {...state, error: action.payload }; 79 | case 'ADD_WORD': 80 | var calendar = state.user.stats.calendar; 81 | calendar = [...calendar]; 82 | var today = calendar[calendar.length - 1]; 83 | today.wordcount += 1; 84 | 85 | return {...state, user: { 86 | ...state.user, 87 | stats:{ 88 | ...state.user.stats, 89 | calendar: calendar, 90 | streak: calculateStreak(calendar) 91 | }}}; 92 | 93 | } 94 | 95 | return state; 96 | } 97 | -------------------------------------------------------------------------------- /client/reducers/trees.reducer.js: -------------------------------------------------------------------------------- 1 | /* import { FETCH_POSTS, FETCH_POST } from '../actions/index';*/ 2 | import { getCard, createCard, updateCard, deleteCard, moveCard } from '../utils/cards'; 3 | import { DEFAULT_TREE } from '../data'; 4 | 5 | var INITIAL_STATE = [] 6 | 7 | export default function(state=INITIAL_STATE, action) { 8 | switch(action.type) { 9 | case 'LIST_TREES': 10 | var trees = action.payload; 11 | 12 | return trees; 13 | case 'DELETE_TREE': 14 | var deletedTree = action.payload; 15 | var trees = state; 16 | console.log(JSON.stringify(trees)); 17 | trees = trees.filter((t)=>{ 18 | return t.slug != deletedTree.slug; 19 | }); 20 | return [...trees]; 21 | 22 | default: 23 | return state; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route, IndexRoute} from 'react-router'; 3 | 4 | import App from './components/App'; 5 | import Main from './components/Main'; 6 | import TreeManager from './components/TreeManager'; 7 | 8 | import RequireAuth from './components/auth/require_auth'; 9 | 10 | /* import PostList from './components/PostList';*/ 11 | 12 | export default ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | -------------------------------------------------------------------------------- /client/styles/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /client/styles/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /client/styles/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /client/styles/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /client/styles/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /client/styles/fonts/foundation-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/foundation-icons.eot -------------------------------------------------------------------------------- /client/styles/fonts/foundation-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/foundation-icons.ttf -------------------------------------------------------------------------------- /client/styles/fonts/foundation-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/foundation-icons.woff -------------------------------------------------------------------------------- /client/styles/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /client/styles/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /client/styles/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /client/styles/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/client/styles/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /client/utils/dragAndDrop.js: -------------------------------------------------------------------------------- 1 | import { getCard, getParent, getAllChildren } from './cards'; 2 | 3 | /* Implements the drag source contract. */ 4 | export const cardSource = { 5 | beginDrag(props) { 6 | // Return the data describing the dragged item 7 | const item = { cardId: props.card.id }; 8 | return item; 9 | }, 10 | 11 | endDrag(props, monitor, component) { 12 | const item = monitor.getItem(); 13 | const dropResult = monitor.getDropResult(); 14 | if (dropResult) { 15 | /* Trigger this once the card is dropped */ 16 | var card = null; 17 | var parent = null; 18 | var children = null; 19 | var childrenIds = null; 20 | 21 | var dropIt = true; 22 | card = getCard(item.cardId,props.tree.cards); 23 | parent = getParent(card, props.tree.cards); 24 | children = getAllChildren(card, props.tree.cards); 25 | childrenIds = children.map((c)=>c.id); 26 | 27 | /* If I'm not dropping the card on itself 28 | or on it's own parent, or on one of it's children. */ 29 | if (item.cardId != dropResult.parentId 30 | && !(parent.id == dropResult.parentId && dropResult.position=="right") 31 | && !childrenIds.includes(dropResult.parentId)) { 32 | /* Trigger action that will move the card */ 33 | /* console.log( `Dropped ${item.cardId} into ${dropResult.parentId}!`);*/ 34 | props.dropCard(dropResult.position, dropResult.parentId, props.card); 35 | } else { 36 | console.log("Cancel DnD. Trying to drop onto itself/parent/children."); 37 | } 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /client/utils/handleScroll.js: -------------------------------------------------------------------------------- 1 | import {TweenMax} from "gsap"; 2 | 3 | import { cardsToColumns, getChildren, getCard, getParent, getAllParents, 4 | getAllChildren, forEachCard, getFirstChildren, getCardsColumn } from './cards'; 5 | 6 | 7 | export function scrollTo(card, column) { 8 | var card = document.getElementById('card-' + card.id); 9 | /* var card = ReactDOM.findDOMNode(card);*/ 10 | var col = document.getElementById('column-'+column) 11 | 12 | /* For search. If couldn't find a card - dont scroll. */ 13 | if (!(card && col)){ return null;} 14 | 15 | var rect = card.getBoundingClientRect(); 16 | var colRect = col.getBoundingClientRect(); 17 | 18 | /* Calculate card's center. (relative to what?) */ 19 | var cardCenter = rect.top + rect.height*0.5; 20 | 21 | /* If I want to scroll the top of the card to almost the top of the column. */ 22 | cardCenter = rect.top + colRect.height/4; 23 | 24 | var scrollTop = col.scrollTop + (cardCenter - col.offsetHeight*0.5) 25 | /* col.scrollTop = scrollTop;*/ 26 | 27 | TweenMax.to(col, 0.15, { scrollTop: scrollTop, ease: Power2.easeInOut }); 28 | } 29 | 30 | 31 | export default function handleScroll(cardId, cards, columns) { 32 | /* console.log(cardId);*/ 33 | var card = getCard(cardId, cards); 34 | /* console.log("Scrolling to card " + JSON.stringify(cardId, null, 4));*/ 35 | var allParents = getAllParents(card, cards); 36 | /* console.log("All parents " + JSON.stringify(allParents));*/ 37 | 38 | var firstChildren = getFirstChildren(card, columns); 39 | /* console.log("First children " + JSON.stringify(allParents));*/ 40 | 41 | var scrolledColumns = []; 42 | 43 | /* Scroll to all of it's parents */ 44 | allParents.map((p)=> { 45 | var cardsColumn = getCardsColumn(p, columns); 46 | scrollTo(p, cardsColumn); 47 | scrolledColumns.push(cardsColumn); 48 | }); 49 | /* Scroll to the active card */ 50 | var column = getCardsColumn(card, columns); 51 | scrollTo(card, column); 52 | scrolledColumns.push(column); 53 | 54 | /* console.log("Scrolling to\n" + card.content.substring(0, 30));*/ 55 | /* Scroll to all of it's first children */ 56 | firstChildren.map((c)=> { 57 | var childsColumn = getCardsColumn(c, columns); 58 | scrollTo(c, childsColumn); 59 | scrolledColumns.push(childsColumn); 60 | }); 61 | 62 | /* After scrolling card, parents, and children, 63 | there might be unscrolled column, because active card has no children in it. 64 | So make sure that every column is scrolled. */ 65 | columns.map((c, i)=>{ 66 | if (scrolledColumns.indexOf(c.index) == -1) { 67 | /* console.log("Found unscrolled column " + c.index);*/ 68 | var firstCard = c.cardGroups[0].cards[0]; 69 | scrollTo(firstCard, c.index); 70 | scrolledColumns.push(c.index); 71 | } 72 | }); 73 | /* console.log("Scrolled columns " + scrolledColumns); */ 74 | } 75 | -------------------------------------------------------------------------------- /client/utils/misc.js: -------------------------------------------------------------------------------- 1 | export function unsavedWarning(e) { 2 | var message = "The tree wasn't saved! Are you sure you want to close tab?"; 3 | e.returnValue = message; 4 | return message; 5 | } 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | function getPlugins() { 4 | const plugins = []; 5 | 6 | if (process.env.NODE_ENV === "development") { 7 | plugins.push(new webpack.DefinePlugin({ 8 | 'process.env.NODE_ENV': JSON.stringify('development') 9 | })); 10 | } 11 | if (process.env.NODE_ENV === "production") { 12 | plugins.push(new webpack.DefinePlugin({ 13 | 'process.env': { 14 | NODE_ENV: JSON.stringify('production') 15 | } 16 | })); 17 | plugins.push(new webpack.optimize.UglifyJsPlugin({ 18 | minimize: true, 19 | output: { 20 | comments: false 21 | }, 22 | compressor: { 23 | warnings: false 24 | } 25 | })); 26 | } 27 | 28 | return plugins; 29 | } 30 | 31 | 32 | module.exports = { 33 | entry: [ 34 | './index.js' 35 | ], 36 | output: { 37 | path: __dirname + '/dist/', 38 | publicPath: '/', 39 | filename: 'bundle.js' 40 | }, 41 | devServer: { 42 | historyApiFallback: true, 43 | }, 44 | module: { 45 | loaders: [ 46 | { 47 | exclude: /node_modules/, 48 | loader: 'babel-loader', 49 | query: { 50 | presets: ['react', 'es2015', 'stage-1'], 51 | plugins: ["transform-decorators-legacy"] 52 | } 53 | }, 54 | { 55 | test: /\.scss$/, 56 | loaders: ['style-loader', 'css-loader', 'sass-loader'] 57 | }, 58 | { 59 | test: /\.css$/, 60 | loader: 'style-loader!css-loader' 61 | }, 62 | { 63 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 64 | loader: 'url-loader?limit=100000' 65 | }, 66 | { 67 | test: /\.json$/, 68 | loader: 'json-loader' 69 | }, 70 | { 71 | test: /\.nls/, 72 | loader: 'raw-loader' 73 | } 74 | ] 75 | }, 76 | plugins: getPlugins(), 77 | resolve: { 78 | extensions: ['.js', '.jsx'] 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /config/#settings.js#: -------------------------------------------------------------------------------- 1 | var settings = { 2 | googleAnalyticsCode: "UA-44003603-21", 3 | }; 4 | 5 | settings.metaSocialImage = settings.domain + "/media/social.png"; 6 | 7 | module.exports = settings; 8 | 9 | -------------------------------------------------------------------------------- /config/.#settings.js: -------------------------------------------------------------------------------- 1 | ray@lumen.3606:1494388886 -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | secret: 'secret-key', 3 | domain: 'https://nulis.io', 4 | stripeSecret: 'test', 5 | }; 6 | -------------------------------------------------------------------------------- /config/nulis_nginx.conf: -------------------------------------------------------------------------------- 1 | map $http_upgrade $connection_upgrade { 2 | default upgrade; 3 | '' close; 4 | } 5 | 6 | server { 7 | # redirect from http to https 8 | listen 80; 9 | listen [::]:80; 10 | server_name nulis.io www.nulis.io; 11 | return 301 https://$server_name$request_uri; 12 | } 13 | 14 | 15 | server { 16 | listen 80; 17 | server_name nulis.io localhost; 18 | 19 | # For SSL verification 20 | # location ~ /.well-known { 21 | # allow all; 22 | # root /var/www/html; 23 | # } 24 | 25 | 26 | # load ssl config 27 | listen 443 ssl http2; 28 | listen [::]:443 ssl http2; 29 | include snippets/ssl-nulis.io.conf; 30 | include snippets/ssl-params.conf; 31 | 32 | keepalive_timeout 70; 33 | sendfile on; 34 | client_max_body_size 0; 35 | 36 | # Enable compression 37 | # gzip off; 38 | gzip on; 39 | gzip_disable "MSIE [1-6]\."; 40 | gzip_comp_level 6; 41 | gzip_min_length 1100; 42 | gzip_buffers 16 8k; 43 | gzip_proxied expired no-cache no-store private auth; 44 | gzip_vary on; 45 | gzip_types 46 | text/plain 47 | text/css 48 | text/js 49 | text/xml 50 | text/javascript 51 | application/javascript 52 | application/x-javascript 53 | application/json 54 | application/xml 55 | application/rss+xml 56 | font/truetype 57 | font/opentype 58 | application/vnd.ms-fontobject 59 | image/svg+xml; 60 | gzip_static on; 61 | 62 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; 63 | 64 | proxy_set_header HOST $host; 65 | proxy_set_header X-Forwarded-Proto $scheme; 66 | proxy_set_header X-Real-IP $remote_addr; 67 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 68 | 69 | proxy_buffering off; 70 | proxy_redirect off; 71 | proxy_http_version 1.1; 72 | proxy_set_header Upgrade $http_upgrade; 73 | proxy_set_header Connection $connection_upgrade; 74 | tcp_nodelay on; 75 | 76 | # Enable caching. 77 | # location ~* \.(jpg|jpeg|png|gif|ico|css|js|otf|ttf|eot|woff|svg)$ { 78 | # expires 10d; 79 | # access_log off; 80 | # ## Fancy extra config recommended by a guy 81 | # add_header Vary Accept-Encoding; 82 | # tcp_nodelay off; 83 | # open_file_cache max=3000 inactive=120s; 84 | # open_file_cache_valid 45s; 85 | # open_file_cache_min_uses 2; 86 | # open_file_cache_errors off; 87 | 88 | # proxy_set_header Host $host; 89 | # proxy_buffering off; 90 | # add_header Pragma public; 91 | # add_header Cache-Control "public"; 92 | 93 | # proxy_pass http://0.0.0.0:3000; 94 | # } 95 | 96 | location ~ .*\.zip$ { 97 | add_header Content-Type "application/zip"; 98 | 99 | try_files $uri $uri/ /Nulis-linux-x64.zip /Nulis-win32-x64.zip /Nulis-darwin-x64.zip =404; 100 | root /home/ray/nulis/desktop/packages/; 101 | } 102 | 103 | location /downloads { 104 | root /home/ray/nulis/desktop/packages; 105 | } 106 | 107 | location / { 108 | proxy_pass http://0.0.0.0:3090; 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /config/settings.js: -------------------------------------------------------------------------------- 1 | var settings = { 2 | googleAnalyticsCode: "UA-44003603-21", 3 | }; 4 | 5 | settings.metaSocialImage = settings.domain + "/media/images/social.png"; 6 | 7 | module.exports = settings; 8 | 9 | -------------------------------------------------------------------------------- /desktop/674f50d287a8c48dc19ba404d20fe713.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/desktop/674f50d287a8c48dc19ba404d20fe713.eot -------------------------------------------------------------------------------- /desktop/b06871f281fee6b241d60582ae9369b9.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/desktop/b06871f281fee6b241d60582ae9369b9.ttf -------------------------------------------------------------------------------- /desktop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nulis 7 | 8 | 9 |
10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /desktop/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { Router, browserHistory } from 'react-router'; 5 | import { createStore, applyMiddleware } from 'redux'; 6 | import reduxThunk from 'redux-thunk'; 7 | 8 | import routes from '../client/routes'; 9 | import reducers from '../client/reducers'; 10 | 11 | import App from '../client/components/App'; 12 | 13 | // Connect reduxThunk to middleware so I could dispatch async actions with axios. 14 | const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore); 15 | 16 | // store contains the state 17 | const store = createStoreWithMiddleware(reducers); 18 | 19 | browserHistory.replace('/') 20 | 21 | ReactDOM.render( 22 | 23 | 24 | 25 | , document.querySelector('.app')); 26 | -------------------------------------------------------------------------------- /desktop/install.sh: -------------------------------------------------------------------------------- 1 | # Electron's version. 2 | export npm_config_target=1.6.2 3 | # The architecture of Electron, can be ia32 or x64. 4 | export npm_config_arch=x64 5 | export npm_config_target_arch=x64 6 | # Download headers for Electron. 7 | export npm_config_disturl=https://atom.io/download/electron 8 | # Tell node-pre-gyp that we are building for Electron. 9 | export npm_config_runtime=electron 10 | # Tell node-pre-gyp to build module from source code. 11 | export npm_config_build_from_source=true 12 | # Install all dependencies, and store cache to ~/.electron-gyp. 13 | HOME=~/.electron-gyp npm install 14 | npm install $1 15 | -------------------------------------------------------------------------------- /desktop/main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const {app, BrowserWindow} = electron; 3 | const ipc = electron.ipcMain 4 | require('electron-debug')({showDevTools: false}); 5 | /* require('electron-reload')(__dirname);*/ 6 | 7 | /* Electron modules */ 8 | const { Menu } = electron; 9 | /* const countdown = require('./utils/index');*/ 10 | 11 | process.env.NODE_ENV = 'desktop'; 12 | /* Defining mainWindow here so I could use it in functions below */ 13 | let mainWindow 14 | 15 | app.on('ready', _=>{ 16 | /* Cerate main window */ 17 | mainWindow = new BrowserWindow({ 18 | height: 400, 19 | width: 400 20 | }) 21 | 22 | /* Hide menu */ 23 | Menu.setApplicationMenu(null) 24 | 25 | /* Load index.html */ 26 | mainWindow.loadURL(`file://${__dirname}/index.html`) 27 | 28 | /* Open dev tools */ 29 | if (process.env.NODE_ENV === 'development') { 30 | mainWindow.webContents.openDevTools() 31 | } 32 | /* localShortcut.register('Ctrl+Shift+I', mainWindow.toggleDevTools());*/ 33 | 34 | /* When window is closed - garbage collect and whatnot */ 35 | mainWindow.on('closed',_=>{ 36 | console.log('closed!') 37 | mainWindow = null 38 | }) 39 | }); 40 | 41 | 42 | /* html button sends the countdown start event */ 43 | ipc.on('countdown-start', _=> { 44 | /* countdown will pass the count variable to this function every second*/ 45 | countdown(count => { 46 | /* once I get it - send the event to the frontend, giving it count */ 47 | mainWindow.webContents.send('countdown',count) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /desktop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "electron .", 9 | "build": "webpack -p --progress && electron-packager . Nulis --electron-version=1.6.2 --platform=linux --arch=x64 --out='./builds/' --overwrite && chmod +x ./builds/Nulis-linux-x64/Nulis", 10 | "buildlinux": "webpack -p --progress && electron-zip-packager . Nulis --electron-version=1.6.2 --platform=linux --arch=x64 --out='./packages/' --overwrite", 11 | "buildall": "webpack -p --progress && electron-zip-packager . Nulis --electron-version=1.6.2 --all --arch=x64 --out='./packages/' --overwrite" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "devtron": "^1.4.0", 17 | "electron-prebuilt": "^1.4.13", 18 | "electron-reload": "^1.1.0" 19 | }, 20 | "dependencies": { 21 | "electron-debug": "^1.1.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /desktop/styles/style.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | height: 100vh; 7 | } 8 | 9 | h1 { 10 | color: blue; 11 | } -------------------------------------------------------------------------------- /desktop/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function countdown(tick) { 2 | let count = 10 3 | 4 | let timer = setInterval(_=>{ 5 | /* Send the count to the function in main.js 6 | which will then send it to the frontend */ 7 | tick(count--) 8 | console.log("count ", count) 9 | if (count === 0) { 10 | clearInterval(timer) 11 | } 12 | }, 1000) 13 | } 14 | -------------------------------------------------------------------------------- /desktop/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | function getPlugins() { 4 | const plugins = []; 5 | 6 | plugins.push(new webpack.DefinePlugin({ 7 | 'process.env.NODE_ENV': JSON.stringify('desktop') 8 | })); 9 | 10 | if (process.env.NODE_ENV === "production") { 11 | plugins.push(new webpack.optimize.UglifyJsPlugin({ 12 | minimize: true, 13 | output: { 14 | comments: false 15 | }, 16 | compressor: { 17 | warnings: false 18 | } 19 | })); 20 | } 21 | 22 | return plugins; 23 | } 24 | 25 | module.exports = { 26 | entry: [ 27 | './index.js' 28 | ], 29 | output: { 30 | path: __dirname, 31 | publicPath: '/', 32 | filename: './dist/bundle.js' 33 | }, 34 | module: { 35 | loaders: [ 36 | { 37 | exclude: /node_modules/, 38 | loader: 'babel', 39 | query: { 40 | presets: ['react', 'es2015', 'stage-1'], 41 | plugins: ["transform-decorators-legacy"] 42 | } 43 | }, 44 | { 45 | test: /\.scss$/, 46 | loaders: ['style', 'css', 'sass'] 47 | }, 48 | { 49 | test: /\.css$/, 50 | loader: 'style-loader!css-loader' 51 | }, 52 | { 53 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 54 | loader: 'url-loader?limit=100000' 55 | }, 56 | { 57 | test: /\.json$/, 58 | loader: 'json-loader' 59 | }, 60 | { 61 | test: /\.nls/, 62 | loader: 'raw-loader' 63 | } 64 | ] 65 | }, 66 | resolve: { 67 | extensions: ['', '.js', '.jsx'] 68 | }, 69 | plugins: getPlugins(), 70 | devServer: { 71 | historyApiFallback: true, 72 | contentBase: './' 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | db: 5 | image: mongo:latest 6 | command: "--smallfiles --logpath=/dev/null" 7 | volumes: 8 | - ./data/db:/data/db 9 | web: 10 | build: . 11 | working_dir: /home/nulis 12 | command: npm run serve 13 | depends_on: 14 | - db 15 | links: 16 | - db 17 | ports: 18 | - "3090:3000" 19 | volumes: 20 | - ./client:/home/nulis/client 21 | - ./server:/home/nulis/server 22 | - ./desktop:/home/nulis/desktop 23 | - ./package.json:/home/nulis/package.json 24 | environment: 25 | MONGO_URL: mongodb://db:27017/nulis 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nulis", 3 | "version": "0.0.1", 4 | "description": "Tree editor for writers.", 5 | "main": "./server/index.js", 6 | "scripts": { 7 | "startserver": "cd ./server && BABEL_DISABLE_CACHE=1 NODE_ENV=development nodemon index.js", 8 | "startclient": "cd ./client && NODE_ENV=development node ../node_modules/webpack-dev-server/bin/webpack-dev-server.js", 9 | "buildserver": "NODE_ENV=production cd ./server && webpack --config ./webpack.config.server.js --progress", 10 | "buildclient": "NODE_ENV=production cd ./client && webpack -p --config ./webpack.config.js --progress", 11 | "build": "NODE_ENV=production cd ./client && webpack -p --config ./webpack.config.js --progress && cd ../server && webpack --config ./webpack.config.server.js --progress", 12 | "serve": "cd ./server && NODE_ENV=production forever ./index.js > log.txt" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/raymestalez/nulis.git" 17 | }, 18 | "author": "Ray Alez", 19 | "license": "AGPL", 20 | "bugs": { 21 | "url": "https://github.com/raymestalez/nulis/issues" 22 | }, 23 | "homepage": "https://github.com/raymestalez/nulis#readme", 24 | "devDependencies": { 25 | "babel-core": "^6.2.1", 26 | "babel-loader": "^6.2.0", 27 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 28 | "babel-plugin-webpack-loaders": "^0.9.0", 29 | "babel-polyfill": "^6.23.0", 30 | "babel-preset-es2015": "^6.1.18", 31 | "babel-preset-react": "^6.1.18", 32 | "babel-preset-stage-1": "^6.1.18", 33 | "css-loader": "^0.28.0", 34 | "json-loader": "^0.5.4", 35 | "raw-loader": "^0.5.1", 36 | "sass-loader": "^6.0.3", 37 | "webpack": "^2.4.1", 38 | "webpack-dev-middleware": "^1.6.1", 39 | "webpack-dev-server": "^2.4.3", 40 | "webpack-externals-plugin": "^1.0.0", 41 | "webpack-hot-middleware": "^2.10.0", 42 | "webpack-manifest-plugin": "^1.0.1" 43 | }, 44 | "dependencies": { 45 | "axios": "^0.15.3", 46 | "babel-register": "^6.24.1", 47 | "bcrypt-nodejs": "0.0.3", 48 | "body-parser": "^1.15.2", 49 | "cors": "^2.8.1", 50 | "cuid": "^1.3.8", 51 | "ejs": "^2.5.6", 52 | "express": "^4.15.2", 53 | "file-loader": "^0.11.1", 54 | "forever": "^0.15.3", 55 | "gsap": "^1.19.1", 56 | "js-cookie": "^2.1.4", 57 | "jwt-simple": "^0.5.0", 58 | "lodash": "^3.10.1", 59 | "memory-cache": "^0.1.6", 60 | "moment": "^2.18.1", 61 | "mongoose": "^4.9.6", 62 | "morgan": "^1.7.0", 63 | "mousetrap": "^1.6.0", 64 | "node-sass": "^4.5.2", 65 | "nodemon": "^1.11.0", 66 | "passport": "^0.3.2", 67 | "passport-jwt": "^2.2.1", 68 | "passport-local": "^1.0.0", 69 | "react": "^0.14.3", 70 | "react-bootstrap": "^0.31.0", 71 | "react-dnd": "^2.3.0", 72 | "react-dnd-html5-backend": "^2.3.0", 73 | "react-dom": "^0.14.3", 74 | "react-dragula": "^1.1.17", 75 | "react-file-input": "^0.2.5", 76 | "react-file-reader-input": "^1.1.0", 77 | "react-ga": "^2.2.0", 78 | "react-meta-tags": "^0.1.3", 79 | "react-redux": "^4.0.0", 80 | "react-router": "^2.0.1", 81 | "react-simplemde-editor": "^3.6.4", 82 | "react-stripe-checkout": "^2.2.5", 83 | "react-textarea-autosize": "^4.0.5", 84 | "redux": "^3.0.4", 85 | "redux-form": "^6.6.3", 86 | "redux-thunk": "^2.1.0", 87 | "redux-undo": "^0.6.1", 88 | "remarkable": "^1.7.1", 89 | "remove-markdown": "^0.1.0", 90 | "save-as": "^0.1.8", 91 | "slug": "^0.9.1", 92 | "snoowrap": "^1.14.0", 93 | "stripe": "^4.18.0", 94 | "style-loader": "^0.16.1", 95 | "url-loader": "^0.5.8", 96 | "validator": "^7.0.0" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /server/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-2"], 3 | "plugins": [], 4 | "env": { 5 | "production": { 6 | "presets": ["es2015", "react", "react-optimize", "es2015-native-modules", "stage-0"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* Entry Script */ 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | // In production, serve the webpacked server file. 5 | require('./dist/server.bundle.js'); 6 | } else { 7 | // Babel polyfill to convert ES6 code in runtime 8 | require('babel-register')({ 9 | "presets": ["react", "es2015", "stage-2"], 10 | "plugins": [ 11 | [ 12 | "babel-plugin-webpack-loaders", 13 | { 14 | "config": "./webpack.config.babel.js", 15 | "verbose": false 16 | } 17 | ] 18 | ], 19 | }); 20 | require('babel-polyfill'); 21 | 22 | require('./server'); 23 | } 24 | -------------------------------------------------------------------------------- /server/initialData.js: -------------------------------------------------------------------------------- 1 | import Tree from './models/tree'; 2 | 3 | var fs = require('fs'); 4 | 5 | var AboutTemplate = JSON.parse(fs.readFileSync('../assets/trees/about.nls', 'utf8')); 6 | var BlankTemplate = JSON.parse(fs.readFileSync('../assets/trees/blank.nls', 'utf8')); 7 | var StoryStructureTemplate = JSON.parse(fs.readFileSync('../assets/trees/storystructure.nls', 'utf8')); 8 | 9 | export default function initialData () { 10 | Tree.count().exec((err, count) => { 11 | if (count > 0) { 12 | return; 13 | } 14 | 15 | const aboutTree = new Tree(AboutTemplate); 16 | const blankTree = new Tree(BlankTemplate); 17 | const storyStructureTree = new Tree(StoryStructureTemplate); 18 | 19 | Tree.create([aboutTree, blankTree, storyStructureTree], (error) => { 20 | if (!error) { 21 | console.log('Initial data loaded.'); 22 | } 23 | }); 24 | }); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /server/log.txt: -------------------------------------------------------------------------------- 1 | warn: --minUptime not set. Defaulting to: 1000ms 2 | warn: --spinSleepTime not set. Your script will exit if it does not stay up for at least 1000ms 3 | Connecting to the db at mongodb://localhost:27017/nulis 4 | Server is running on port 3000! 5 | -------------------------------------------------------------------------------- /server/misc/#hotprompts.py#: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import json 3 | import pickle 4 | import praw 5 | from praw.models import Comment, User 6 | from datetime import datetime 7 | import pytz 8 | from dateutil.relativedelta import relativedelta 9 | from retrying import retry 10 | 11 | 12 | 13 | # Connect to praw 14 | def connect(): 15 | # Load config 16 | with open('config.json', 'r') as f: 17 | config = json.load(f) 18 | 19 | r = praw.Reddit( 20 | client_id = config["client_id"], 21 | client_secret = config["client_secret"], 22 | user_agent='Best /r/WritingPrompts authors by /u/raymestalez' 23 | ) 24 | return r 25 | 26 | 27 | def age(timestamp): 28 | created_at = datetime.utcfromtimestamp(timestamp) 29 | now = datetime.utcnow() 30 | age_in_minutes = int((now-created_at).total_seconds())/60 31 | return age_in_minutes 32 | 33 | 34 | def get_prompts(): 35 | new_prompts = list(subreddit.new(limit=100)) 36 | hot_prompts = list(subreddit.hot(limit=50)) 37 | prompts = [] 38 | 39 | # 5 hours to minutes 40 | max_age = 5*60 41 | # less than 5 replies, more than 1 upvote and less than max_age old 42 | for prompt in new_prompts: 43 | if (prompt.score > 1) \ 44 | and ((prompt.num_comments-1) < 3) \ 45 | and (age(prompt.created_utc) < max_age) \ 46 | and ("[OT]" not in prompt.title): 47 | # print("New prompt" + str(prompt.__dict__)) 48 | if prompt.num_comments > 0: 49 | prompt.num_comments -= 1 # remove fake reply 50 | # Prompt age, like 3.5 hours 51 | prompt.age = round(age(prompt.created_utc)/60,1) 52 | 53 | # prompt position 54 | for index, p in enumerate(hot_prompts): 55 | # Find a prompt on the front page 56 | if prompt.title == p.title: 57 | # prompt.position == index 58 | setattr(prompt, "position", index-1) 59 | 60 | prompt.title = prompt.title.replace("[WP]", "", 1).strip() 61 | prompt.title = prompt.title.replace("[EU]", "", 1).strip() 62 | prompts.append(prompt) 63 | 64 | # sort by score 65 | prompts.sort(key=lambda p: p.score, reverse=True) 66 | 67 | 68 | # Save as json 69 | prompts_list = [] 70 | for prompt in prompts[:10]: 71 | # print(prompt.title) 72 | prompt_dict = {} 73 | prompt_dict['title'] = prompt.title 74 | prompt_dict['url'] = prompt.url 75 | try: 76 | prompt_dict['position'] = prompt.position 77 | except: 78 | prompt.position = 0 79 | prompt_dict['score'] = prompt.score 80 | prompt_dict['num_comments'] = prompt.num_comments 81 | prompt_dict['age'] = prompt.age 82 | prompts_list.append(prompt_dict) 83 | prompts_json = json.dumps(prompts_list) 84 | with open('hotprompts.json', "w") as text_file: 85 | text_file.write(prompts_json) 86 | 87 | return prompts[:10] 88 | 89 | 90 | 91 | r = connect() 92 | subreddit = r.subreddit('writingprompts') 93 | get_prompts() 94 | -------------------------------------------------------------------------------- /server/misc/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_id": "A2ebDtZubqwKKg", 3 | "client_secret": "HI0C_jsaoUiDs4261V2sktpUOwc" 4 | } 5 | -------------------------------------------------------------------------------- /server/misc/hotprompts.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const snoowrap = require('snoowrap'); 3 | 4 | /* const config = JSON.parse(fs.readFileSync('../config.json','utf8'));*/ 5 | 6 | const r = new snoowrap({ 7 | userAgent: 'Best /r/WritingPrompts prompts by /u/raymestalez', 8 | clientId: 'STVPWGT5kl4t7Q', 9 | clientSecret: 'CLLwhfaHzitTf0dZNcyI20Xfikg', 10 | username: 'raymestalez', 11 | password: 'Qwe1s1zxCrdt' 12 | }); 13 | 14 | 15 | export default function get_prompts(fun) { 16 | r.getSubreddit('WritingPrompts').getNew({limit: 100}).then((newPrompts)=>{ 17 | r.getSubreddit('WritingPrompts').getHot({limit: 50}).then((hotPrompts)=>{ 18 | var prompts = newPrompts.map((prompt)=>{ 19 | /* Calculate age */ 20 | var now = new Date().getTime(); 21 | var created_at = new Date(prompt.created_utc*1000); 22 | var age_minutes = (now - created_at) / (1000 * 60); 23 | var age_hours = Math.round(age_minutes/60 * 10) / 10 24 | 25 | prompt.age = age_hours; 26 | prompt.num_comments -= 1; 27 | 28 | /* Prompts with >1 upvote, <3 replies, less than 5 hours old */ 29 | if (prompt.score > 1 30 | && prompt.num_comments < 3 31 | && age_hours < 5 32 | && !prompt.title.includes("[OT]")) { 33 | 34 | prompt.position = 0; 35 | /* Search for a prompt in the list of hot prompts, 36 | to find out it's position on the front page */ 37 | hotPrompts.map((hotPrompt,i)=>{ 38 | if (prompt.title == hotPrompt.title) { 39 | prompt.position = i-1; 40 | } 41 | }); 42 | prompt.title = prompt.title.replace("[WP]","").trim(); 43 | prompt.title = prompt.title.replace("[EU]","").trim(); 44 | /* Add prompt to the list of prompts */ 45 | return prompt; 46 | } 47 | }); 48 | 49 | /* Sort by score */ 50 | prompts.sort((a,b)=>{ 51 | return parseFloat(b.score) - parseFloat(a.score); 52 | }); 53 | 54 | /* Return 15 prompts */ 55 | prompts = prompts.slice(0,10); 56 | fun(prompts); 57 | }); 58 | }); 59 | } 60 | 61 | /* 62 | var prompts = get_prompts((prompts)=>{ 63 | console.log(prompts.length); 64 | prompts.map((p)=> console.log(p.score)); 65 | }); 66 | */ 67 | -------------------------------------------------------------------------------- /server/misc/hotprompts.json: -------------------------------------------------------------------------------- 1 | [{"position": 9, "age": 3.4, "title": "In exchange for a cure to save his wife - a man agreed to work for the Devil as Death. As death he begins to notice things; like how his wife is a serial killer.", "url": "https://www.reddit.com/r/WritingPrompts/comments/6bfw5i/wp_in_exchange_for_a_cure_to_save_his_wife_a_man/", "num_comments": 0, "score": 24}, {"position": 14, "age": 2.0, "title": "After hackers threaten to leak their newest movie, Disney hires an elite group of mercenaries to discourage future attempts. Due to a typo however, the mercenaries assume they have to disguise as Disney characters", "url": "https://www.reddit.com/r/WritingPrompts/comments/6bg6f2/wp_after_hackers_threaten_to_leak_their_newest/", "num_comments": 2, "score": 15}, {"position": 20, "age": 2.2, "title": "Humans can have super powers, however these powers are not everlasting and can be used up. This has lead to power-use addicts, and means that 'heroes' and 'villains' are temporary at best.", "url": "https://www.reddit.com/r/WritingPrompts/comments/6bg55v/wp_humans_can_have_super_powers_however_these/", "num_comments": 0, "score": 10}, {"position": 25, "age": 3.6, "title": "The broken clock that's right three times a day.", "url": "https://www.reddit.com/r/WritingPrompts/comments/6bfuq2/wp_the_broken_clock_thats_right_three_times_a_day/", "num_comments": 1, "score": 9}, {"position": 19, "age": 0.7, "title": "Write a story about a supervillian who is unspeakably more powerful than anyone else on his planet, but is content with using it for small things like cutting in line or getting free extra servings.", "url": "https://www.reddit.com/r/WritingPrompts/comments/6bgf9e/wpwrite_a_story_about_a_supervillian_who_is/", "num_comments": 0, "score": 8}, {"position": 27, "age": 3.9, "title": "You've invented time travel. You decide to test it out by going back in time to your favorite era. However, when you arrive, several locals say \"Oh, you're back! How have you been?\"", "url": "https://www.reddit.com/r/WritingPrompts/comments/6bfsbs/wp_youve_invented_time_travel_you_decide_to_test/", "num_comments": 0, "score": 7}, {"position": 30, "age": 2.5, "title": "Helen launched a thousand ships. Cleopatra used her charm to fight Rome. Write a modern day story where a beautiful woman/man is the cause of WW3. No gods or magic involved.", "url": "https://www.reddit.com/r/WritingPrompts/comments/6bg2tp/wp_helen_launched_a_thousand_ships_cleopatra_used/", "num_comments": 2, "score": 6}, {"position": 36, "age": 4.0, "title": "Using only the Internet, aliens try to find out what humans enjoy and what items they require in order to be happy.", "url": "https://www.reddit.com/r/WritingPrompts/comments/6bfr8o/wp_using_only_the_internet_aliens_try_to_find_out/", "num_comments": 0, "score": 6}, {"position": 0, "age": 0.5, "title": "Everything you will ever need to know about time travel paradoxes, in one easy to read information leaflet... [WP]", "url": "https://www.reddit.com/r/WritingPrompts/comments/6bgh1b/everything_you_will_ever_need_to_know_about_time/", "num_comments": 1, "score": 4}, {"position": 38, "age": 2.0, "title": "A clown, tired of all the negative and monstrous depictions of his chosen profession, decides to do something about it. Along the way, he goes on to become the world's greatest hero.", "url": "https://www.reddit.com/r/WritingPrompts/comments/6bg6m3/wp_a_clown_tired_of_all_the_negative_and/", "num_comments": 0, "score": 4}] -------------------------------------------------------------------------------- /server/misc/hotprompts.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import json 3 | import pickle 4 | import praw 5 | from praw.models import Comment, User 6 | from datetime import datetime 7 | import pytz 8 | from dateutil.relativedelta import relativedelta 9 | from retrying import retry 10 | 11 | 12 | 13 | # Connect to praw 14 | def connect(): 15 | # Load config 16 | with open('config.json', 'r') as f: 17 | config = json.load(f) 18 | 19 | r = praw.Reddit( 20 | client_id = config["client_id"], 21 | client_secret = config["client_secret"], 22 | user_agent='Best /r/WritingPrompts authors by /u/raymestalez' 23 | ) 24 | return r 25 | 26 | 27 | def age(timestamp): 28 | created_at = datetime.utcfromtimestamp(timestamp) 29 | now = datetime.utcnow() 30 | age_in_minutes = int((now-created_at).total_seconds())/60 31 | return age_in_minutes 32 | 33 | 34 | def get_prompts(): 35 | new_prompts = list(subreddit.new(limit=100)) 36 | hot_prompts = list(subreddit.hot(limit=50)) 37 | prompts = [] 38 | 39 | # 5 hours to minutes 40 | max_age = 5*60 41 | # less than 5 replies, more than 1 upvote and less than max_age old 42 | for prompt in new_prompts: 43 | if (prompt.score > 1) \ 44 | and ((prompt.num_comments-1) < 3) \ 45 | and (age(prompt.created_utc) < max_age) \ 46 | and ("[OT]" not in prompt.title): 47 | # print("New prompt" + str(prompt.__dict__)) 48 | if prompt.num_comments > 0: 49 | prompt.num_comments -= 1 # remove fake reply 50 | # Prompt age, like 3.5 hours 51 | prompt.age = round(age(prompt.created_utc)/60,1) 52 | 53 | # prompt position 54 | for index, p in enumerate(hot_prompts): 55 | # Find a prompt on the front page 56 | if prompt.title == p.title: 57 | # prompt.position == index 58 | setattr(prompt, "position", index-1) 59 | 60 | prompt.title = prompt.title.replace("[WP]", "", 1).strip() 61 | prompt.title = prompt.title.replace("[EU]", "", 1).strip() 62 | prompts.append(prompt) 63 | 64 | # sort by score 65 | prompts.sort(key=lambda p: p.score, reverse=True) 66 | 67 | 68 | # Save as json 69 | prompts_list = [] 70 | for prompt in prompts[:10]: 71 | # print(prompt.title) 72 | prompt_dict = {} 73 | prompt_dict['title'] = prompt.title 74 | prompt_dict['url'] = prompt.url 75 | try: 76 | prompt_dict['position'] = prompt.position 77 | except: 78 | prompt.position = 0 79 | prompt_dict['score'] = prompt.score 80 | prompt_dict['num_comments'] = prompt.num_comments 81 | prompt_dict['age'] = prompt.age 82 | prompts_list.append(prompt_dict) 83 | prompts_json = json.dumps(prompts_list) 84 | with open('hotprompts.json', "w") as text_file: 85 | text_file.write(prompts_json) 86 | 87 | return prompts[:10] 88 | 89 | 90 | 91 | r = connect() 92 | subreddit = r.subreddit('writingprompts') 93 | get_prompts() 94 | -------------------------------------------------------------------------------- /server/models/#tree.js#: -------------------------------------------------------------------------------- 1 | /* Mongoose is ORM, like models.py in django */ 2 | const mongoose = require('mongoose'); 3 | const Schema = mongoose.Schema; 4 | 5 | // Define model. 6 | const treeSchema = new Schema({ 7 | slug: { 8 | type: String, 9 | unique: true, 10 | required: true, 11 | lowercase: true, 12 | index: true, 13 | }, 14 | author: { 15 | type: String, 16 | required: true, 17 | lowercase: true, 18 | index: true, 19 | }, 20 | name: { 21 | type: String, 22 | unique: false, 23 | required: true, 24 | minlength: 1, 25 | trim: true, 26 | index: true 27 | }, 28 | createdAt: { 29 | type: Date, 30 | default: null 31 | }, 32 | updatedAt: { 33 | type: Date, 34 | default: null 35 | }, 36 | cards: { 37 | type: JSON, 38 | unique: false, 39 | required: false, 40 | minlength: 1 41 | }, 42 | activeCard: { 43 | type: String, 44 | default: 0 45 | }, 46 | modified: { 47 | type: Boolean, 48 | default: false 49 | }, 50 | editing: { 51 | type: Boolean, 52 | default: false 53 | } 54 | }); 55 | /* 56 | owner: ownerId or email? 57 | */ 58 | 59 | treeSchema.set('autoIndex', false); 60 | 61 | 62 | treeSchema.pre('save', function(next){ 63 | var now = new Date(); 64 | this.updatedAt = now; 65 | if ( !this.createdAt ) { 66 | this.createdAt = now; 67 | } 68 | next(); 69 | }); 70 | // Create model class 71 | const TreeModel = mongoose.model('tree', treeSchema); 72 | 73 | // Export model 74 | module.exports = TreeModel; 75 | 76 | -------------------------------------------------------------------------------- /server/models/tree.js: -------------------------------------------------------------------------------- 1 | /* Mongoose is ORM, like models.py in django */ 2 | const mongoose = require('mongoose'); 3 | const Schema = mongoose.Schema; 4 | 5 | // Define model. 6 | const treeSchema = new Schema({ 7 | slug: { 8 | type: String, 9 | unique: true, 10 | required: true, 11 | lowercase: true, 12 | index: true, 13 | }, 14 | author: { 15 | type: String, 16 | required: true, 17 | lowercase: true, 18 | index: true, 19 | }, 20 | name: { 21 | type: String, 22 | unique: false, 23 | required: true, 24 | minlength: 1, 25 | trim: true, 26 | index: true 27 | }, 28 | createdAt: { 29 | type: Date, 30 | default: null 31 | }, 32 | updatedAt: { 33 | type: Date, 34 | default: null 35 | }, 36 | cards: { 37 | type: JSON, 38 | unique: false, 39 | required: false, 40 | minlength: 1 41 | }, 42 | activeCard: { 43 | type: String, 44 | default: 0 45 | }, 46 | modified: { 47 | type: Boolean, 48 | default: false 49 | }, 50 | editing: { 51 | type: Boolean, 52 | default: false 53 | } 54 | }); 55 | /* 56 | owner: ownerId or email? 57 | */ 58 | 59 | treeSchema.set('autoIndex', false); 60 | 61 | 62 | treeSchema.pre('save', function(next){ 63 | var now = new Date(); 64 | this.updatedAt = now; 65 | if ( !this.createdAt ) { 66 | this.createdAt = now; 67 | } 68 | next(); 69 | }); 70 | // Create model class 71 | const TreeModel = mongoose.model('tree', treeSchema); 72 | 73 | // Export model 74 | module.exports = TreeModel; 75 | 76 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | /* Mongoose is ORM, like models.py in django */ 2 | import mongoose, {Schema} from 'mongoose'; 3 | import validator from 'validator'; 4 | import bcrypt from 'bcrypt-nodejs'; 5 | import cuid from 'cuid'; 6 | 7 | // Define model. 8 | const userSchema = new Schema({ 9 | email: { 10 | type: String, 11 | unique: true, 12 | required: true, 13 | trim: true, 14 | lowercase: true, 15 | minlength: 1, 16 | validate: { 17 | validator: validator.isEmail, 18 | message: '{VALUE} is not a valid email' 19 | } 20 | }, 21 | password: { 22 | type: String, 23 | required: true, 24 | minlength: 4 25 | }, 26 | referralCode: { 27 | type: String, 28 | default: "" 29 | }, 30 | referral: { 31 | type: String, 32 | default: "" 33 | }, 34 | source: { 35 | type: String, 36 | default: "" 37 | }, 38 | plan: { 39 | type: String, 40 | default: "Free" 41 | }, 42 | cardLimit: { 43 | type: Number, 44 | default: 200 45 | }, 46 | invited: { 47 | type: Number, 48 | default: 0 49 | }, 50 | createdAt: { 51 | type: Date, 52 | default: null 53 | }, 54 | stats: { 55 | type: JSON, 56 | default: { 57 | calendar: [] 58 | } 59 | } 60 | }); 61 | 62 | 63 | // On save hook, encrypt password 64 | // Before saving a model, run this function 65 | userSchema.pre('save', function(next){ 66 | // get access to the user model. User is an instance of the user model. 67 | const user = this; 68 | 69 | if (this.isNew) { 70 | console.log("Created new user, hashing password") 71 | this.createdAt = new Date(); 72 | this.referralCode = cuid.slug(); 73 | // generate a salt, then run callback. 74 | bcrypt.genSalt(10, function(err, salt){ 75 | if (err) { return next(err); } 76 | // hash(encrypt) the password using the salt 77 | bcrypt.hash(user.password, salt, null, function(err, hash){ 78 | if (err) { return next(err); } 79 | // override plain text password with encrypted password 80 | user.password = hash; 81 | 82 | next(); 83 | }); 84 | }); 85 | } else { 86 | console.log("Updated user.") 87 | next(); 88 | } 89 | }); 90 | 91 | // This is like defining a function on the model in models.py 92 | userSchema.methods.comparePassword = function(candidatePassword, callback) { 93 | /* console.log("pwd1 " + candidatePassword);*/ 94 | /* console.log("pwd2 " + this.password); */ 95 | bcrypt.compare(candidatePassword, this.password, function(err, isMatch){ 96 | if (err) { return callback(err); } 97 | console.log("isMatch " + isMatch); 98 | callback(null, isMatch); 99 | }); 100 | }; 101 | 102 | // Create model class 103 | const ModelClass = mongoose.model('user', userSchema); 104 | 105 | // Export model 106 | module.exports = ModelClass; 107 | -------------------------------------------------------------------------------- /server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "ignore": [ 4 | ".git", 5 | "node_modules/**/node_modules" 6 | ], 7 | "verbose": true, 8 | "env": { 9 | "NODE_ENV": "development" 10 | }, 11 | "ext": "js json" 12 | } 13 | -------------------------------------------------------------------------------- /server/routes/profiles.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | const router = new Router(); 3 | 4 | const passport = require('passport'); 5 | const passportService = require('../services/passport'); 6 | 7 | const requireAuth = passport.authenticate('jwt', { session:false }); 8 | const requireSignin = passport.authenticate('local', { session:false }); 9 | 10 | const profilesControllers = require('../controllers/profiles.controllers.js'); 11 | 12 | // Make every request go through the passport profilesentication check: 13 | router.route('/auth-test').get(requireAuth, function(req, res){ 14 | console.log("req " + JSON.stringify(req.user)); 15 | res.send({ message:'Successfully accessed protected API!'}); 16 | }); 17 | 18 | /* Take a request from a url and send a response. */ 19 | router.route('/auth/join').post(profilesControllers.signup); 20 | router.route('/auth/login').post(requireSignin, profilesControllers.signin); 21 | 22 | router.route('/auth/profile').get(requireAuth, profilesControllers.getUser); 23 | /* router.route('/purchase').post(requireAuth, profilesControllers.payment);*/ 24 | router.route('/purchase/:email').post(profilesControllers.paypal_payment); 25 | router.route('/update-wordcount').post(requireAuth, profilesControllers.updateWordcount); 26 | 27 | 28 | export default router; 29 | 30 | -------------------------------------------------------------------------------- /server/routes/trees.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as treeControllers from '../controllers/tree.controllers'; 3 | const router = new Router(); 4 | 5 | const passport = require('passport'); 6 | const passportService = require('../services/passport'); 7 | const requireAuth = passport.authenticate('jwt', { session:false }); 8 | 9 | router.route('/trees').get(requireAuth, treeControllers.listTrees); 10 | router.route('/trees').post(requireAuth, treeControllers.createTree); 11 | router.route('/tree/:slug').get(treeControllers.getTree); 12 | router.route('/tree/:slug').post(requireAuth, treeControllers.updateTree); 13 | router.route('/tree/:slug').delete(requireAuth, treeControllers.deleteTree); 14 | 15 | 16 | export default router; 17 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import http from 'http'; 3 | import bodyParser from 'body-parser'; // Parse requests, turn them into json 4 | import morgan from 'morgan'; // A logging framework, terminal output for debugging. 5 | import mongoose from 'mongoose'; // ORM between mongo and node. 6 | import cors from 'cors'; // Cors allows requests from different domains 7 | import path from 'path'; // manipulate filepaths 8 | import util from 'util'; 9 | 10 | 11 | /* Routes */ 12 | import profilesRoutes from './routes/profiles.routes.js'; 13 | import treesRoutes from './routes/trees.routes.js'; 14 | 15 | /* Controllers */ 16 | import * as treeControllers from './controllers/tree.controllers'; 17 | 18 | 19 | // Connect to db. 20 | mongoose.Promise = global.Promise; 21 | var MONGO_DB_URL = process.env.MONGO_URL || 'mongodb://localhost:27017/nulis'; 22 | console.log("Connecting to the db at " + MONGO_DB_URL); 23 | mongoose.connect(MONGO_DB_URL, (error) => { 24 | if (error) { 25 | console.error('Please make sure Mongodb is installed and running!'); 26 | throw error; 27 | } 28 | }); 29 | 30 | /* Setup server */ 31 | const server = new Express(); 32 | server.use(bodyParser.json({type: '*/*'})); 33 | server.use(cors()); 34 | server.set('view engine', 'ejs'); 35 | /* server.use(morgan('combined'));*/ 36 | 37 | 38 | 39 | /* API */ 40 | server.use('/api/v1', profilesRoutes); 41 | server.use('/api/v1', treesRoutes); 42 | 43 | /* Serve static files */ 44 | server.use('/media', 45 | Express.static(path.resolve(__dirname, '../client/media'))); 46 | server.use('/downloads', 47 | Express.static(path.resolve(__dirname, '../desktop/packages'))); 48 | server.get('/bundle.js',(req,res) => { 49 | res.sendFile(path.resolve(__dirname, '../client/dist/bundle.js')); 50 | }); 51 | 52 | /* Static pages */ 53 | server.use('/static', 54 | Express.static(path.resolve(__dirname, './static'))); 55 | // index page 56 | import fs from 'fs'; 57 | server.get('/top-writingprompts-authors', function(req, res) { 58 | var top_authors = JSON.parse(fs.readFileSync('./misc/top_authors_week.json','utf8')); 59 | res.render('leaderboard', {authors: top_authors, timeframe:'week', loc:'authors'}); 60 | }); 61 | server.get('/top-writingprompts-authors/alltime', function(req, res) { 62 | var top_authors = JSON.parse(fs.readFileSync('./misc/top_authors_all.json','utf8')); 63 | res.render('leaderboard', {authors: top_authors, timeframe:'all', loc:'authors'}); 64 | }); 65 | 66 | /* Cache */ 67 | var mcache = require('memory-cache'); 68 | var cache = (duration) => { 69 | return (req, res, next) => { 70 | let key = '__express__' + req.originalUrl || req.url 71 | let cachedBody = mcache.get(key) 72 | if (cachedBody) { 73 | res.send(cachedBody) 74 | return 75 | } else { 76 | res.sendResponse = res.send 77 | res.send = (body) => { 78 | mcache.put(key, body, duration * 1000); 79 | res.sendResponse(body) 80 | } 81 | next() 82 | } 83 | } 84 | } 85 | 86 | import get_prompts from './misc/hotprompts'; 87 | /* 5*60 */ 88 | server.get('/prompts', cache(5*60), function(req, res) { 89 | /* var prompts = JSON.parse(fs.readFileSync('./misc/hotprompts.json', 'utf8'));*/ 90 | get_prompts((prompts)=>{ 91 | res.render('prompts', {prompts: prompts, loc:'prompts'}); 92 | }); 93 | /* var prompts = require('./misc/hotprompts.json');*/ 94 | 95 | }); 96 | 97 | /* Export */ 98 | server.get('/tree/:slug.md',treeControllers.exportTree); 99 | 100 | /* Send the rest of the requests to be handled by the react router */ 101 | server.use((req, res) => 102 | res.sendFile(path.resolve(__dirname, '../client/index.html'))); 103 | 104 | // start server 105 | const port = process.env.PORT || 3000; 106 | server.listen(port, (error) => { 107 | if (!error) { 108 | console.log(`Server is running on port ${port}!`); 109 | } else { 110 | console.error('Couldnt start server!'); 111 | } 112 | }); 113 | 114 | export default server; 115 | -------------------------------------------------------------------------------- /server/services/passport.js: -------------------------------------------------------------------------------- 1 | // authentication layer, before the protected routes 2 | // check if user is logged in before accessing controllers(which are like django views) 3 | // So this is essentially @IsAuthenticated 4 | 5 | const passport = require('passport'); 6 | const JwtStrategy = require('passport-jwt').Strategy; 7 | const LocalStrategy = require('passport-local'); 8 | const ExtractJwt = require('passport-jwt').ExtractJwt; 9 | const User = require('../models/user'); 10 | const config = require('../../config/config.js'); 11 | 12 | 13 | 14 | // by default you send a POST request with username and password 15 | // here Im telling it to use email instead 16 | const localOptions = { usernameField: 'email'}; 17 | // Create local strategy. 18 | const localLogin = new LocalStrategy(localOptions, function(email,password,done){ 19 | console.log("Checking username and password. If they match - pass person in."); 20 | 21 | // Verify username/password 22 | // Call done with the user if it's correct 23 | // otherwise call done with false. 24 | User.findOne({email:email}, function(err,user){ 25 | if (err) { return done(err); } 26 | /* if username not found */ 27 | if (!user) { 28 | console.log("User not found. " + email) 29 | return done(null, false); 30 | } 31 | 32 | //compare passwords using the function I've defined in user model 33 | user.comparePassword(password, function(err, isMatch){ 34 | if (err) { return done(err); } 35 | // if passwords don't match 36 | if (!isMatch) { 37 | console.log("Passwords don't match. "); 38 | console.log(email); 39 | return done(null, false); 40 | } 41 | 42 | // return user without errors 43 | return done(null, user); 44 | }); 45 | }) 46 | }); 47 | 48 | // Set up options for jwt strategies 49 | //(ways to authenticate, like with token or username/password) 50 | // tell it where to look for token 51 | // and secret used to decode the token 52 | const jwtOptions = { 53 | jwtFromRequest: ExtractJwt.fromHeader('authorization'), 54 | secretOrKey: config.secret 55 | }; 56 | 57 | // Create JWT Strategy for token authentication 58 | const jwtLogin = new JwtStrategy(jwtOptions, function(payload, done){ 59 | // payload is a decoded JWT token, sub and iat from the token. 60 | // done is a callback, depending on whether auth is successful 61 | 62 | /* console.log("JWT login");*/ 63 | // See if user id from payload exists in our database 64 | // If it does call 'done' with that user 65 | // otherwise, call 'done' without a user object 66 | User.findById(payload.sub, function(err, user){ 67 | if (err) { return done(err, false); } 68 | /* console.log("Found user! "); */ 69 | if (user) { 70 | console.log("JWT login successful! "); 71 | done(null, user); 72 | } else { 73 | console.log("JWT Login unsuccessful. Probably wrong JWT. "); 74 | done(null, false); 75 | } 76 | }); 77 | 78 | }); 79 | 80 | /* console.log("jwtLogin " + JSON.stringify(jwtLogin));*/ 81 | 82 | // Tell passport to use JWT strategy 83 | passport.use(jwtLogin); 84 | passport.use(localLogin); 85 | -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/_bootstrap.min.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/_bootstrap.min.scssc -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/_font-awesome.min.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/_font-awesome.min.scssc -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/_foundation-icons.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/_foundation-icons.scssc -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/_opensans.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/_opensans.scssc -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/_simplemde.min.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/_simplemde.min.scssc -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/style.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/06808200605fc810748fdc2898494e825f912dd6/style.scssc -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/_bootstrap.min.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/_bootstrap.min.scssc -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/_font-awesome.min.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/_font-awesome.min.scssc -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/_foundation-icons.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/_foundation-icons.scssc -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/_opensans.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/_opensans.scssc -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/_simplemde.min.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/_simplemde.min.scssc -------------------------------------------------------------------------------- /server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/style.scssc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/.sass-cache/ed8a4160aae535ff6f003dad5dbeabcb3c5c12cf/style.scssc -------------------------------------------------------------------------------- /server/static/styles/config.rb: -------------------------------------------------------------------------------- 1 | require 'compass/import-once/activate' 2 | 3 | css_dir = "." 4 | sass_dir = "." 5 | 6 | output_style = :compressed 7 | 8 | 9 | -------------------------------------------------------------------------------- /server/static/styles/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /server/static/styles/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /server/static/styles/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /server/static/styles/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /server/static/styles/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /server/static/styles/fonts/foundation-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/foundation-icons.eot -------------------------------------------------------------------------------- /server/static/styles/fonts/foundation-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/foundation-icons.ttf -------------------------------------------------------------------------------- /server/static/styles/fonts/foundation-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/foundation-icons.woff -------------------------------------------------------------------------------- /server/static/styles/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /server/static/styles/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /server/static/styles/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /server/static/styles/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/nulis/db8d0325be148fcf44f29bfe0de445bbb7f940c7/server/static/styles/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /server/views/#listprompts.ejs#: -------------------------------------------------------------------------------- 1 | <% for(var i=0; i < prompts.length; i++) { %> 2 | 34 | <% } %> 35 | -------------------------------------------------------------------------------- /server/views/elements/footer.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | -------------------------------------------------------------------------------- /server/views/elements/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <% if(loc=='authors'){ %> 6 | Top WritingPrompts Authors 7 | 8 | 10 | 12 | 14 | 15 | 16 | 17 | <% } else{ %> 18 | Unnoticed Prompts 19 | 20 | 22 | 24 | 26 | 27 | 28 | 29 | <% } %> 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /server/views/elements/header.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 10 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /server/views/leaderboard.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% include ./elements/head %> 5 | 6 | 7 |
8 | <% include ./elements/header %> 9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 | 17 | <% if(timeframe=='week'){ %> 18 | This Week 19 | <% } else{ %> 20 | All Time 21 | <% } %> 22 | 29 | 30 | 31 |

Top 100 /r/WritingPrompts authors

32 | 33 | This is a leaderboard of the best(most upvoted) 34 | /r/WritingPrompts authors and their top stories.
35 | <% if(timeframe=='week'){ %> 36 | Updated every day. 37 | <% } else{ %> 38 | Updated every week. 39 | <% } %> 40 | 41 |
42 |
43 | <%# 44 | Check out other cool stuff that I've made: 45 |
46 | - Discover 47 | 48 | new prompts with very few replies that are about to take off. 49 |
50 | - Learn how to use Nulis and /r/WritingPrompts to craft your stories, get awesome at writing fiction, and develop daily writing habits. 51 |
52 | - Use the story writing template to brainstorm and outline your stories. 53 | %> 54 |
55 | <% include ./listauthors %> 56 |
57 |
58 |
59 |
60 |
61 | <% include ./elements/footer %> 62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /server/views/listauthors.ejs: -------------------------------------------------------------------------------- 1 | <% for(var i=0; i < authors.length; i++) { %> 2 | 20 | <% } %> 21 | -------------------------------------------------------------------------------- /server/views/listprompts.ejs: -------------------------------------------------------------------------------- 1 | <% for(var i=0; i < prompts.length; i++) { %> 2 | 34 | <% } %> 35 | -------------------------------------------------------------------------------- /server/views/prompts.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% include ./elements/head %> 5 | 6 | 7 |
8 | <% include ./elements/header %> 9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 | 18 | Top writers 19 | 20 |

Unnoticed Prompts

21 | Use this tool to discover great prompts that did not receive many replies. 22 | Replying to them will help your story to get more attention(it will not be buried under hundreds of other replies), and will make sure that prompts people want to read are not left unanswered. 23 | <%# 24 | Check out other cool stuff that I've made: 25 |
26 | - 27 | Discover top 100 /r/WritingPrompts authors and their best stories. 28 | 29 |
30 | - Learn how to use Nulis and /r/WritingPrompts to craft your stories, get awesome at writing fiction, and develop daily writing habits. 31 |
32 | - Use the story writing template to brainstorm and outline your stories. 33 | %> 34 |
35 | <% include ./listprompts %> 36 |
37 |
38 |
39 |
40 |
41 | 42 | 47 | 48 | 49 | <% include ./elements/footer %> 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /server/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | output: { 3 | publicPath: '/', 4 | libraryTarget: 'commonjs2', 5 | }, 6 | resolve: { 7 | extensions: ['.js', '.jsx'], 8 | modules: [ 9 | 'node_modules', 10 | ], 11 | }, 12 | module: { 13 | loaders: [ 14 | /* 15 | { 16 | test: /\.scss$/, 17 | loaders: ['css-loader', 'sass-loader'] 18 | }, 19 | { 20 | test: /\.css$/, 21 | loader: 'style-loader!css-loader' 22 | }, 23 | */ 24 | { 25 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 26 | loader: 'url-loader?limit=100000' 27 | }, 28 | { 29 | test: /\.json$/, 30 | loader: 'json-loader' 31 | }, 32 | ], 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /server/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var ExternalsPlugin = require('webpack-externals-plugin'); 4 | 5 | module.exports = { 6 | 7 | entry: path.resolve(__dirname, 'server.js'), 8 | 9 | output: { 10 | path: __dirname + '/dist/', 11 | filename: 'server.bundle.js', 12 | libraryTarget: 'commonjs', 13 | }, 14 | externals: [ 15 | /^(?!\.|\/).+/i, 16 | ], 17 | 18 | target: 'node', 19 | 20 | node: { 21 | __filename: true, 22 | __dirname: true, 23 | }, 24 | 25 | resolve: { 26 | extensions: ['.js', '.jsx'], 27 | modules: [ 28 | 'client', 29 | 'node_modules', 30 | ], 31 | }, 32 | 33 | module: { 34 | loaders: [ 35 | { 36 | test: /\.js$/, 37 | exclude: /node_modules/, 38 | loader: 'babel-loader', 39 | query: { 40 | presets: [ 41 | 'react', 42 | 'es2015', 43 | 'stage-0', 44 | ], 45 | plugins: [ 46 | [ 47 | 'babel-plugin-webpack-loaders', { 48 | 'config': './webpack.config.babel.js', 49 | "verbose": false 50 | } 51 | ] 52 | ] 53 | }, 54 | }, { 55 | test: /\.json$/, 56 | loader: 'json-loader', 57 | }, 58 | ], 59 | }, 60 | plugins: [ 61 | new ExternalsPlugin({ 62 | type: 'commonjs', 63 | include: path.join(__dirname, '../node_modules/'), 64 | }), 65 | ], 66 | }; 67 | --------------------------------------------------------------------------------