├── client ├── src │ ├── assets │ │ ├── scss │ │ │ ├── src │ │ │ │ ├── partials │ │ │ │ │ └── buttons.scss │ │ │ │ ├── layouts │ │ │ │ │ ├── footer.scss │ │ │ │ │ ├── admin.scss │ │ │ │ │ └── listing.scss │ │ │ │ ├── components │ │ │ │ │ ├── submit.scss │ │ │ │ │ ├── breadcrumbs.scss │ │ │ │ │ └── pagination.scss │ │ │ │ ├── helpers │ │ │ │ │ ├── _variables.scss │ │ │ │ │ ├── _qe-menubar.scss │ │ │ │ │ └── _fonts.scss │ │ │ │ ├── main.scss │ │ │ │ └── misc.scss │ │ │ └── README.md │ │ ├── img │ │ │ ├── logo.png │ │ │ ├── avatar │ │ │ │ ├── 1.png │ │ │ │ ├── 10.jpg │ │ │ │ ├── 2.jpg │ │ │ │ ├── 3.jpg │ │ │ │ ├── 4.jpg │ │ │ │ ├── 5.jpg │ │ │ │ ├── 6.jpg │ │ │ │ ├── 7.jpg │ │ │ │ ├── 8.png │ │ │ │ └── 9.jpg │ │ │ ├── notes-logo.ai │ │ │ ├── gray-square.png │ │ │ ├── julia-logo.png │ │ │ ├── loading-dots.gif │ │ │ ├── notes-logo.png │ │ │ ├── python-logo.png │ │ │ ├── default-avatar.png │ │ │ ├── loadingSpinner.gif │ │ │ ├── light-gray-square.jpg │ │ │ ├── logo │ │ │ │ ├── julia-logo.png │ │ │ │ ├── landing-sloan-logo.png │ │ │ │ ├── landing-jupyter-logo.png │ │ │ │ ├── landing-quantecon-logo.png │ │ │ │ └── landing-sloan-logo-inverted.png │ │ │ ├── qe-menubar-icons.png │ │ │ ├── quant-econ-avatar.png │ │ │ ├── qe-logo-horizontal.png │ │ │ └── notes-logo.svg │ │ └── fonts │ │ │ ├── foundation-icons.eot │ │ │ ├── foundation-icons.ttf │ │ │ └── foundation-icons.woff │ ├── index.css │ ├── reducers │ │ ├── Submit.js │ │ ├── index.js │ │ ├── User.js │ │ ├── Utils.js │ │ ├── Announcements.js │ │ ├── SubmissionList.js │ │ └── EditSubmission.js │ ├── remark-math │ │ ├── index.js │ │ ├── package.json │ │ └── inline.js │ ├── components │ │ ├── App │ │ │ ├── App.test.js │ │ │ ├── App.css │ │ │ └── App.jsx │ │ ├── Notification.jsx │ │ ├── NotebookFromHTML.jsx │ │ ├── Image.jsx │ │ ├── auth │ │ │ └── SignInButton.js │ │ ├── partials │ │ │ ├── Breadcrumbs.jsx │ │ │ ├── MetaTags.jsx │ │ │ └── Notebook.jsx │ │ ├── TempComponent.jsx │ │ ├── MarkdownMathJax.jsx │ │ ├── Sitemap.jsx │ │ ├── ProtectedRoute.jsx │ │ ├── Contact.jsx │ │ ├── AdminRoute.jsx │ │ ├── submissions │ │ │ ├── EditSubmission.js │ │ │ └── SubmissionList.jsx │ │ ├── NotFound.jsx │ │ ├── comments │ │ │ ├── Reply.js │ │ │ └── ReplyList.js │ │ ├── About.jsx │ │ ├── user │ │ │ └── UserPreview.jsx │ │ ├── admin │ │ │ ├── RemoveSubmissionModal.jsx │ │ │ └── AddAdminModal.jsx │ │ ├── home │ │ │ └── Home.jsx │ │ └── FAQ.jsx │ ├── index.js │ ├── store │ │ ├── store.js │ │ └── configure-store.js │ ├── imageData.json │ ├── utils │ │ ├── url.js │ │ ├── localStorage.js │ │ ├── popup.js │ │ └── trimText.js │ ├── containers │ │ ├── AnnouncementsContainer.jsx │ │ ├── user │ │ │ ├── MyProfileContainer.js │ │ │ ├── UserContainer.js │ │ │ └── EditProfileContainer.js │ │ ├── auth │ │ │ └── OAuthSignInButton.js │ │ ├── SubmitContainer.js │ │ ├── HeadContainer.jsx │ │ ├── submission │ │ │ ├── EditSubmissionPreviewContainer.js │ │ │ ├── SubmissionListContainer.jsx │ │ │ ├── EditSubmissionContainer.js │ │ │ └── SubmissionContainer.js │ │ ├── PreviewContainer.js │ │ └── comment │ │ │ └── CommentContainer.jsx │ └── actions │ │ ├── auth │ │ └── signOut.js │ │ ├── utils.js │ │ ├── user.js │ │ ├── editSubmission.js │ │ └── submissionList.js ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── tests │ └── dataframe.jpg ├── .gitignore └── package.json ├── server ├── robots.txt ├── assets │ ├── invite-template.html │ ├── nbconvert │ │ └── templates │ │ │ └── notebookHTML.tpl │ └── aboutPage.md ├── js │ ├── db │ │ ├── models │ │ │ ├── EmailList.js │ │ │ ├── AdminList.js │ │ │ ├── Announcement.js │ │ │ ├── Comment.js │ │ │ ├── Submission.js │ │ │ └── User.js │ │ ├── migrations │ │ │ ├── template.js │ │ │ ├── 1547430906567-test-migrate.js │ │ │ ├── 1548822198972-delete-empty-comment.js │ │ │ ├── 1555396627360-resize-avatars-to-50px-google.js │ │ │ ├── 1555396634463-resize-avatars-to-50px-github.js │ │ │ ├── 1545351944251-add-authorName-Submissions.js │ │ │ └── 1555330744208-resize-avatars-to-50px-twitter.js │ │ ├── .migrate │ │ ├── README.md │ │ └── mongoose.js │ ├── auth │ │ ├── init.js │ │ ├── jwt.js │ │ └── adminjwt.js │ ├── sorting.js │ ├── languages.js │ ├── render.js │ └── sitemap.js ├── routes │ ├── auth │ │ ├── isAuthenticated.js │ │ ├── signOut.js │ │ ├── fb.js │ │ └── twitter.js │ ├── invite.js │ └── restore │ │ └── restore.js └── package.json ├── .dockerignore ├── scripts ├── backup_db.sh ├── util │ └── util-input.sh ├── dev-start.sh └── dev-install.sh ├── .gitignore ├── jsdoc-config.json ├── Dockerfile-server ├── docker-compose.yml ├── backup.sh └── package.json /client/src/assets/scss/src/partials/buttons.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/public/favicon.ico -------------------------------------------------------------------------------- /server/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: /admin 3 | 4 | Sitemap: https://YOUR-HOSTNAME/sitemap.xml 5 | -------------------------------------------------------------------------------- /client/tests/dataframe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/tests/dataframe.jpg -------------------------------------------------------------------------------- /client/src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/logo.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | server/node_modules 2 | server/npm-debug.log 3 | client/build 4 | client/node_modules 5 | client/docs 6 | -------------------------------------------------------------------------------- /client/src/reducers/Submit.js: -------------------------------------------------------------------------------- 1 | import { 2 | BEGIN_SUBMIT, 3 | END_SUBMIT 4 | } from '../actions/submit'; 5 | 6 | -------------------------------------------------------------------------------- /client/src/assets/img/avatar/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/avatar/1.png -------------------------------------------------------------------------------- /client/src/assets/img/avatar/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/avatar/10.jpg -------------------------------------------------------------------------------- /client/src/assets/img/avatar/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/avatar/2.jpg -------------------------------------------------------------------------------- /client/src/assets/img/avatar/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/avatar/3.jpg -------------------------------------------------------------------------------- /client/src/assets/img/avatar/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/avatar/4.jpg -------------------------------------------------------------------------------- /client/src/assets/img/avatar/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/avatar/5.jpg -------------------------------------------------------------------------------- /client/src/assets/img/avatar/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/avatar/6.jpg -------------------------------------------------------------------------------- /client/src/assets/img/avatar/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/avatar/7.jpg -------------------------------------------------------------------------------- /client/src/assets/img/avatar/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/avatar/8.png -------------------------------------------------------------------------------- /client/src/assets/img/avatar/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/avatar/9.jpg -------------------------------------------------------------------------------- /client/src/assets/img/notes-logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/notes-logo.ai -------------------------------------------------------------------------------- /client/src/assets/img/gray-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/gray-square.png -------------------------------------------------------------------------------- /client/src/assets/img/julia-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/julia-logo.png -------------------------------------------------------------------------------- /client/src/assets/img/loading-dots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/loading-dots.gif -------------------------------------------------------------------------------- /client/src/assets/img/notes-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/notes-logo.png -------------------------------------------------------------------------------- /client/src/assets/img/python-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/python-logo.png -------------------------------------------------------------------------------- /scripts/backup_db.sh: -------------------------------------------------------------------------------- 1 | DIR=`date +%m-%d-%y` 2 | DEST=/db_backups/$DIR 3 | mkdir $DEST 4 | mongodump -h localhost -d Bookshelf -o $DEST -------------------------------------------------------------------------------- /client/src/assets/img/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/default-avatar.png -------------------------------------------------------------------------------- /client/src/assets/img/loadingSpinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/loadingSpinner.gif -------------------------------------------------------------------------------- /client/src/assets/img/light-gray-square.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/light-gray-square.jpg -------------------------------------------------------------------------------- /client/src/assets/img/logo/julia-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/logo/julia-logo.png -------------------------------------------------------------------------------- /client/src/assets/img/qe-menubar-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/qe-menubar-icons.png -------------------------------------------------------------------------------- /client/src/assets/img/quant-econ-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/quant-econ-avatar.png -------------------------------------------------------------------------------- /client/src/assets/fonts/foundation-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/fonts/foundation-icons.eot -------------------------------------------------------------------------------- /client/src/assets/fonts/foundation-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/fonts/foundation-icons.ttf -------------------------------------------------------------------------------- /client/src/assets/fonts/foundation-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/fonts/foundation-icons.woff -------------------------------------------------------------------------------- /client/src/assets/img/qe-logo-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/qe-logo-horizontal.png -------------------------------------------------------------------------------- /client/src/assets/img/logo/landing-sloan-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/logo/landing-sloan-logo.png -------------------------------------------------------------------------------- /client/src/assets/img/logo/landing-jupyter-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/logo/landing-jupyter-logo.png -------------------------------------------------------------------------------- /client/src/assets/img/logo/landing-quantecon-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/logo/landing-quantecon-logo.png -------------------------------------------------------------------------------- /client/src/assets/img/logo/landing-sloan-logo-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantEcon/Bookshelf/master/client/src/assets/img/logo/landing-sloan-logo-inverted.png -------------------------------------------------------------------------------- /server/assets/invite-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= title %> 4 | 5 | welcome <%= user%>; 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/src/remark-math/index.js: -------------------------------------------------------------------------------- 1 | const inlinePlugin = require('./inline') 2 | const blockPlugin = require('./block') 3 | 4 | module.exports = function mathPlugin (opts = {}) { 5 | blockPlugin.call(this, opts) 6 | inlinePlugin.call(this, opts) 7 | } 8 | -------------------------------------------------------------------------------- /client/src/components/App/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/components/Notification.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | 3 | class Notification extends Component { 4 | render(){ 5 | return( 6 |
7 | This is a notification 8 |
9 | ) 10 | } 11 | } -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html" 12 | } 13 | -------------------------------------------------------------------------------- /server/js/db/models/EmailList.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema 3 | var ObjectId = Schema.ObjectId 4 | 5 | var emailList = new Schema({ 6 | emails: Array, 7 | name: String 8 | }) 9 | 10 | module.exports = mongoose.model('EmailList', emailList) -------------------------------------------------------------------------------- /server/js/db/models/AdminList.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var ObjectId = Schema.ObjectId; 4 | 5 | var AdminList = new Schema({ 6 | adminEmails: [String], 7 | adminIDs: [ObjectId] 8 | }) 9 | 10 | module.exports = mongoose.model('AdminList', AdminList) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | npm-debug.log 5 | frontend 6 | data/ 7 | files/ 8 | client/src/assets/MathJax 9 | client/src/assets/css/ 10 | server/.node-persist/ 11 | 12 | #config 13 | _config.js 14 | 15 | .DS_store 16 | docs 17 | 18 | docs/ 19 | .ipynb_checkpoints/ 20 | server/js/db/.migrate 21 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App/App'; 4 | import {Provider} from 'react-redux' 5 | import store from './store/store'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , document.getElementById('root')); 11 | -------------------------------------------------------------------------------- /client/src/components/App/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | -------------------------------------------------------------------------------- /client/src/assets/scss/src/layouts/footer.scss: -------------------------------------------------------------------------------- 1 | // Template footer styles 2 | 3 | .footer { 4 | margin:4rem 0 0 0; 5 | background: #fff; 6 | padding:4rem 0 2rem 0; 7 | border-top: 1px solid rgba(10, 10, 10, 0.25); 8 | ul { 9 | list-style: none; 10 | margin:0; 11 | } 12 | a { 13 | font-weight: bold; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /jsdoc-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"], 3 | "opts": { 4 | "template": "./client/node_modules/docdash" 5 | }, 6 | "source": { 7 | "include": ["./client/src/components", "./client/src/actions", "./client/src/containers", "./client/src/utils", "./client/src/store"], 8 | "excludePattern": "(^|\\/|\\\\)_" 9 | } 10 | } -------------------------------------------------------------------------------- /server/js/db/models/Announcement.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var ObjectId = Schema.ObjectId; 4 | 5 | var announcementSchema = new Schema({ 6 | date: Date, 7 | content: String, 8 | postedBy: ObjectId, 9 | recent: Boolean 10 | }) 11 | 12 | module.exports = mongoose.model('Announcement', announcementSchema) -------------------------------------------------------------------------------- /client/src/assets/scss/src/components/submit.scss: -------------------------------------------------------------------------------- 1 | // Submit component styles 2 | 3 | .detected-language { 4 | margin: 0; 5 | border: 1px solid #cacaca; 6 | background: #fff; 7 | border-radius: 5px; 8 | padding-left: 0.5em; 9 | height: 45px; 10 | } 11 | 12 | .notebook-language { 13 | padding-left: 0.5em; 14 | font-size: 14px; 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/src/components/NotebookFromHTML.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | import renderHTML from 'react-render-html' 4 | 5 | class NotebookFromHTML extends Component { 6 | render() { 7 | return ( 8 |
9 | {renderHTML(this.props.html)} 10 |
11 | ) 12 | } 13 | } 14 | 15 | export default NotebookFromHTML -------------------------------------------------------------------------------- /client/src/store/store.js: -------------------------------------------------------------------------------- 1 | import createStore from './configure-store'; 2 | 3 | var store = createStore({ 4 | submissionByID: { 5 | isFetching: true 6 | }, 7 | submissionList: { 8 | isFetching: true 9 | }, 10 | editSubmissionByID: {}, 11 | adminData: { 12 | fetching: true 13 | }, 14 | auth: { 15 | loading: true 16 | } 17 | }) 18 | 19 | export default store; -------------------------------------------------------------------------------- /client/src/assets/scss/README.md: -------------------------------------------------------------------------------- 1 | SCSS is being compiled to css by using `node-sass-chokidar`. 2 | 3 | * `watch-css` script in `client/package.json`is run while running `npm start`, which watches for changes in `main.scss` inside the `src` folder in this 4 | directory and compiles it to `main.css` in `src/assets/css`. 5 | 6 | * `build-css` script in `client/package.json` is run while running `npm run build` which produces a compressed version of `main.css`. 7 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | /src/assets/js/MathJax 24 | 25 | docs 26 | docs/ 27 | -------------------------------------------------------------------------------- /server/js/auth/init.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tlyon on 5/26/17. 3 | */ 4 | 5 | var passport = require('passport'); 6 | var User = require('../db/models/User'); 7 | 8 | module.exports = function () { 9 | passport.serializeUser(function (user, done) { 10 | done(null, user.id); 11 | }); 12 | 13 | passport.deserializeUser(function (id, done) { 14 | User.findById(id, function (err, user) { 15 | done(err, user); 16 | }); 17 | }); 18 | }; -------------------------------------------------------------------------------- /Dockerfile-server: -------------------------------------------------------------------------------- 1 | FROM node:9.5.0 2 | 3 | RUN mkdir /server 4 | WORKDIR /server 5 | ENV NODE_ENV=production 6 | 7 | 8 | COPY server/package.json /server 9 | RUN npm install 10 | 11 | # Copy required server files 12 | COPY ./server/js /server/js 13 | COPY ./server/routes /server/routes 14 | COPY ./server/app.js /server 15 | COPY ./server/robots.txt /server 16 | COPY ./server/_config.js /server 17 | COPY ./server/assets /server/assets 18 | 19 | EXPOSE 8080 20 | 21 | CMD ["npm", "start"] 22 | -------------------------------------------------------------------------------- /client/src/imageData.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "image":"qe-logo-100.svg", 5 | "class_name": "quantecon-logo", 6 | "image_url": "https://quantecon.org/" 7 | }, 8 | { 9 | "image":"landing-jupyter-logo.png", 10 | "class_name": "jupyter-logo", 11 | "image_url": "https://jupyter.org/" 12 | }, 13 | { 14 | "image":"landing-sloan-logo.png", 15 | "class_name": "sloan-logo", 16 | "image_url": "https://sloan.org/" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | server: 4 | container_name: notes 5 | restart: always 6 | image: "bookshelf-server:0.1" 7 | ports: 8 | - "8080:8080" 9 | links: 10 | - mongodb 11 | volumes: 12 | - ./client/build:/client/build 13 | mongodb: 14 | restart: always 15 | container_name: mongodb 16 | image: mongo:latest 17 | ports: 18 | - "27017:27017" 19 | volumes: 20 | - /data/db:/data/db 21 | -------------------------------------------------------------------------------- /server/routes/auth/isAuthenticated.js: -------------------------------------------------------------------------------- 1 | // const passport = require('../../js/auth/jwt'); 2 | 3 | var isAuthenticated = function (req, res, next) { 4 | if (req.isAuthenticated()) { 5 | return next(); 6 | } else { 7 | //not authenticated 8 | console.log('not authenticated'); 9 | if (/^\/user\/my-profile/.test(req.url)) { 10 | res.redirect('/login'); 11 | } 12 | else { 13 | return next(); 14 | } 15 | } 16 | }; 17 | 18 | 19 | 20 | module.exports = { 21 | isAuthenticated, 22 | }; -------------------------------------------------------------------------------- /client/src/components/Image.jsx: -------------------------------------------------------------------------------- 1 | //Image Component requires from imageData.json file in client folder 2 | 3 | import React from 'react'; 4 | 5 | let Image = function statelessFunctionComponentClass(props) { 6 | 7 | let imageName = props.source.image; 8 | let className = props.source.class_name; 9 | let imageUrl = props.source.image_url; 10 | let source = require(`../assets/img/logo/${imageName}`); 11 | 12 | return ( 13 |
  • {className}/
  • 14 | ); 15 | }; 16 | 17 | export default Image; 18 | -------------------------------------------------------------------------------- /backup.sh: -------------------------------------------------------------------------------- 1 | MONGO_DATABASE="notes" 2 | APP_NAME="Notes" 3 | 4 | MONGO_HOST="127.0.0.1" 5 | MONGO_PORT="27017" 6 | TIMESTAMP=`date +%F-%H%M` 7 | MONGODUMP_PATH="/usr/bin/mongodump" 8 | BACKUPS_DIR="$HOME/db_backups" 9 | BACKUP_NAME="$APP_NAME-$TIMESTAMP" 10 | 11 | mongodump -d $MONGO_DATABASE 12 | 13 | mkdir $BACKUPS_DIR 14 | mv dump $BACKUP_NAME 15 | tar -zcvf $BACKUPS_DIR/$BACKUP_NAME.tgz $BACKUP_NAME 16 | rm -rf $BACKUP_NAME 17 | 18 | cd $BACKUPS_DIR 19 | # note: make sure the dir is setup correctly for the bitbucket repository 20 | git add $BACKUP_NAME.tgz 21 | git commit -m "Daily backup" -------------------------------------------------------------------------------- /client/src/components/auth/SignInButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class SignInButton extends Component { 4 | // constructor(props){ 5 | // super(props); 6 | // console.log('[SignInButton] - props: ', props); 7 | 8 | // } 9 | 10 | render(){ 11 | return( 12 |
    13 | 14 | {this.props.icon} 15 | {this.props.provider} 16 | 17 |
    18 | ) 19 | } 20 | } 21 | 22 | export default SignInButton -------------------------------------------------------------------------------- /client/src/components/partials/Breadcrumbs.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | 4 | class Breadcrumbs extends Component { 5 | render() { 6 | return ( 7 | 8 |
    9 | 10 | 16 | 17 |
    18 | ) 19 | } 20 | } 21 | 22 | export default Breadcrumbs 23 | -------------------------------------------------------------------------------- /client/src/components/TempComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Markdown from 'react-markdown'; 3 | 4 | var cleanMarkdown = "# Heading \n **bolded** [link](http://www.google.com)"; 5 | 6 | class TempComponent extends Component { 7 | render(){ 8 | return( 9 |
    10 | loading.... 11 | 12 | 13 |
    14 | ) 15 | } 16 | } 17 | 18 | export default TempComponent 19 | -------------------------------------------------------------------------------- /server/routes/auth/signOut.js: -------------------------------------------------------------------------------- 1 | var passport = require('../../js/auth/twitter'); 2 | var express = require('express'); 3 | var isAuthenticated = require('./isAuthenticated').isAuthenticated; 4 | const jwtAuth = require('../../js/auth/jwt'); 5 | const jwt = require('jsonwebtoken'); 6 | const qs = require('query-string'); 7 | 8 | var app = express.Router(); 9 | 10 | app.get('/', jwtAuth.authenticate('jwt', {session:false}), (req, res) => { 11 | //TODO: invalidate token 12 | console.log("user signed out!"); 13 | // destory the session 14 | req.session.destroy(); 15 | // logout req.user 16 | req.logout(); 17 | res.sendStatus(200); 18 | }); 19 | 20 | module.exports = app; 21 | -------------------------------------------------------------------------------- /client/src/utils/url.js: -------------------------------------------------------------------------------- 1 | export const getParamByName = (name, url) => { 2 | if (!url) { 3 | url = window.location.href; 4 | } 5 | name = name.replace(/[[\]]/g, "\\$&"); 6 | var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), 7 | results = regex.exec(url); 8 | if (!results) return null; 9 | if (!results[2]) return ''; 10 | return decodeURIComponent(results[2].replace(/\+/g, " ")); 11 | } 12 | 13 | export const parseResponse = (response) => { 14 | let json = response.json(); 15 | if (response.status >= 200 && response.status <= 300) { 16 | return json; 17 | } else { 18 | return json.then(err => Promise.reject(err)); 19 | } 20 | } -------------------------------------------------------------------------------- /client/src/components/MarkdownMathJax.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactMarkdown from "react-markdown"; 3 | import MathJax from "react-mathjax"; 4 | import RemarkMathPlugin from "../remark-math/index" 5 | 6 | export const MarkdownRender = (props: ReactMarkdown.ReactMarkdownProps) => { 7 | const newProps = { 8 | ...props, 9 | plugins: [ 10 | RemarkMathPlugin, 11 | ], 12 | renderers: { 13 | ...props.renderers, 14 | math: (props: {value: string}) => 15 | {props.value}, 16 | inlineMath: (props: {value: string}) => 17 | {props.value}, 18 | } 19 | }; 20 | return ( 21 | 22 | 23 | 24 | ); 25 | }; -------------------------------------------------------------------------------- /server/js/db/models/Comment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tlyon on 5/26/17. 3 | */ 4 | 5 | var mongoose = require('mongoose'); 6 | var mongoosePaginate = require('mongoose-paginate'); 7 | 8 | var Schema = mongoose.Schema; 9 | var ObjectId = Schema.ObjectId; 10 | 11 | var commentSchema = new Schema({ 12 | author: ObjectId, 13 | timestamp: Date, 14 | submission: ObjectId, 15 | 16 | score: Number, 17 | 18 | content: String, 19 | 20 | edited: Boolean, 21 | editedDate: Date, 22 | deletedDate: Date, 23 | 24 | replies: [ObjectId], 25 | 26 | flagged: Boolean, 27 | flaggedReason: String, 28 | deleted: Boolean, 29 | 30 | isReply: Boolean, 31 | parentID: ObjectId 32 | 33 | }); 34 | 35 | commentSchema.plugin(mongoosePaginate) 36 | 37 | module.exports = mongoose.model('Comment', commentSchema); 38 | -------------------------------------------------------------------------------- /client/src/assets/scss/src/components/breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | // Breadcrumb styles 2 | 3 | .breadcrumbs { 4 | padding:2rem 0 0 0; 5 | margin:0; 6 | list-style: none; 7 | &::before { 8 | display: table; 9 | content: ' '; 10 | } 11 | &::after { 12 | display: table; 13 | content: ' '; 14 | clear: both; 15 | } 16 | li { 17 | float: left; 18 | font-size: $bc-font-size; 19 | 20 | color: $color-light; 21 | cursor: default; 22 | &:not(:last-child)::after { 23 | position: relative; 24 | top: 1px; 25 | margin: 0 0.75rem; 26 | opacity: 1; 27 | content: "/"; 28 | color: $bc-color-slash; 29 | } 30 | } 31 | a { 32 | font-weight:bold; 33 | &:hover { 34 | text-decoration: underline; 35 | } 36 | } 37 | .disabled { 38 | color: $bc-color-slash; 39 | cursor: not-allowed; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import SubmissionListReducer from './SubmissionList'; 3 | import SubmissionReducer from './Submission'; 4 | import EditSubmissionReducer from './EditSubmission'; 5 | import UserReducer from './User' 6 | import AdminReducer from './Admin' 7 | import AnnouncementReducer from './Announcements' 8 | // import {authStateReducer} from 'redux-auth'; 9 | import AuthReducer from './auth/AuthReducer' 10 | import {LogReducer} from './Utils' 11 | 12 | const rootReducer = combineReducers({ 13 | submissionList: SubmissionListReducer, 14 | submissionByID: SubmissionReducer, 15 | userByID: UserReducer, 16 | auth: AuthReducer, 17 | editSubmissionByID: EditSubmissionReducer, 18 | logs: LogReducer, 19 | adminData: AdminReducer, 20 | announcements: AnnouncementReducer 21 | }); 22 | 23 | export default rootReducer; -------------------------------------------------------------------------------- /client/src/components/Sitemap.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | class Sitemap extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | content: 'loading...' 8 | } 9 | console.log('[About] - fetching sitemap page'); 10 | axios 11 | .get('/api/sitemap') 12 | .then(resp => { 13 | console.log('[About] - returned resp: ', resp.data); 14 | this.setState({ content: resp.data}) 15 | }) 16 | .catch(err => { 17 | console.log('[About] - err:', err); 18 | this.setState({ error: err }) 19 | }) 20 | } 21 | render() { 22 | return ( 23 |
    24 | {this.state.content} 25 |
    26 | ) 27 | } 28 | } 29 | export default Sitemap -------------------------------------------------------------------------------- /server/js/db/migrations/template.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // modules ================================== 4 | var mongoose = require('mongoose'); 5 | var config = require('./../_config'); 6 | 7 | // config =================================== 8 | const options = { 9 | autoReconnect: true, 10 | reconnectTries: 5, 11 | reconnectInterval: 2000 12 | } 13 | 14 | module.exports.up = function (next) { 15 | return mongoose.connect(config.url, options).then(db => { 16 | // perform db operations here 17 | }) 18 | .then(() => { 19 | mongoose.close() 20 | return next() 21 | }) 22 | .catch(err => next(err)) 23 | } 24 | 25 | module.exports.down = function (next) { 26 | return mongoose.connect(config.url, options).then(db => { 27 | // perform db operations here 28 | }) 29 | .then(() => { 30 | mongoose.close() 31 | return next() 32 | }) 33 | .catch(err => next(err)) 34 | } -------------------------------------------------------------------------------- /server/js/db/.migrate: -------------------------------------------------------------------------------- 1 | { 2 | "lastRun": "1555396634463-resize-avatars-to-50px-github.js", 3 | "migrations": [ 4 | { 5 | "title": "1545351944251-add-authorName-Submissions.js", 6 | "timestamp": 1547430178063 7 | }, 8 | { 9 | "title": "1547430906567-test-migrate.js", 10 | "timestamp": 1547430953686 11 | }, 12 | { 13 | "title": "1548822198972-delete-empty-comment.js", 14 | "timestamp": 1554963027196 15 | }, 16 | { 17 | "title": "1555330744208-resize-avatars-to-50px-twitter.js", 18 | "timestamp": 1555397056013 19 | }, 20 | { 21 | "title": "1555396627360-resize-avatars-to-50px-google.js", 22 | "timestamp": 1555397056025 23 | }, 24 | { 25 | "title": "1555396634463-resize-avatars-to-50px-github.js", 26 | "timestamp": 1555400062254 27 | }, 28 | { 29 | "title": "template.js", 30 | "timestamp": 1547430178072 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /server/js/db/migrations/1547430906567-test-migrate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // modules ================================== 4 | var mongoose = require('mongoose'); 5 | var config = require('./../_config'); 6 | 7 | // config =================================== 8 | const options = { 9 | autoReconnect: true, 10 | reconnectTries: 5, 11 | reconnectInterval: 2000 12 | } 13 | 14 | module.exports.up = function (next) { 15 | return mongoose.connect(config.url, options).then(db => { 16 | // perform db operations here 17 | }) 18 | .then(() => { 19 | mongoose.close() 20 | return next() 21 | }) 22 | .catch(err => next(err)) 23 | } 24 | 25 | module.exports.down = function (next) { 26 | return mongoose.connect(config.url, options).then(db => { 27 | // perform db operations here 28 | }) 29 | .then(() => { 30 | mongoose.close() 31 | return next() 32 | }) 33 | .catch(err => next(err)) 34 | } -------------------------------------------------------------------------------- /client/src/components/partials/MetaTags.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import MetaTags from 'react-meta-tags'; 3 | 4 | class MetaTag extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | render: false 9 | } 10 | } 11 | componentDidMount() { 12 | window.setTimeout(()=> { 13 | this.setState({ 14 | render: true 15 | }) 16 | },0) 17 | } 18 | render() { 19 | return ( 20 |
    21 | {this.state.render ? 22 | 23 | {this.props.title} 24 | 25 | 26 | 27 | 28 | : null} 29 |
    30 | ) 31 | } 32 | } 33 | 34 | export default MetaTag; -------------------------------------------------------------------------------- /server/routes/invite.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var passport = require('../js/auth/jwt'); 3 | var validator = require("email-validator"); 4 | 5 | var app = express.Router(); 6 | const notificationTypes = require("../js/notifications").notificationTypes 7 | const sendInvite = require('../js/notifications').sendInvite 8 | 9 | 10 | app.post('/', passport.authenticate('jwt', { 11 | session: 'false' 12 | }), (req, res) => { 13 | console.log(req.body.inviteEmail); 14 | console.log(req.user.name); 15 | 16 | const inviteEmail = req.body.inviteEmail; 17 | // Validate email syntax 18 | if (validator.validate(inviteEmail)) { 19 | // Email is valid 20 | sendInvite(req.body.inviteEmail, req.user.name) 21 | return res.json({validEmail: inviteEmail, emailTruthValue: true}); 22 | } 23 | else { 24 | // Email is not valid 25 | return res.status(404).json({emailError: 'Email is invalid!', emailTruthValue: false}); 26 | } 27 | 28 | }) 29 | 30 | module.exports = app; 31 | -------------------------------------------------------------------------------- /client/src/assets/img/notes-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /server/js/db/README.md: -------------------------------------------------------------------------------- 1 | ## DATABASE MIGRATION 2 | 3 | As QuantEcon evolves with time, so will its existing database. We are currently using `node-migrate` package for writing automatic migration scripts. It will run all the migrations which have not been run yet and skip the completed ones. Migration scripts are stored in migrations folder. 4 | 5 | **NOTE :-** 6 | * In case there is an error of `migrate` command not found, then use `./../../node_modules/migrate/bin/migrate`in `server/js/db`, or you can install the package globally with `npm install migrate -g`. 7 | 8 | #### Installation 9 | * Run `npm install` in server folder. 10 | 11 | #### Creating files 12 | 13 | * Please use the existing template while creating migration files by running `migrate create test-migrate --template-file migrations/template.js` in `server/js/db`. 14 | 15 | #### Running migrations 16 | 17 | * Type `npm run migrate` to run migrations in `server/js/db`. 18 | 19 | **TODO** 20 | 21 | - [ ] Integrating migration command to the build process 22 | -------------------------------------------------------------------------------- /client/src/components/ProtectedRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route, Redirect} from 'react-router-dom' 3 | import store from '../store/store' 4 | import {getParamByName} from '../utils/url'; 5 | import TempComponent from './TempComponent'; 6 | 7 | export default function PrivateRoute({ 8 | component: Component, 9 | ...rest 10 | }) { 11 | const state = store.getState(); 12 | const fromAPI = getParamByName('fromAPI', window.location.href); 13 | return ( 14 | state.auth.isSignedIn === true || fromAPI || state.auth.loading 17 | ?
    18 | {fromAPI 19 | ? 20 | :} 21 |
    22 | : }/> 29 | ) 30 | } -------------------------------------------------------------------------------- /client/src/components/Contact.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import HeadContainer from '../containers/HeadContainer'; 3 | import Breadcrumbs from './partials/Breadcrumbs' 4 | 5 | class Contact extends Component { 6 | render() { 7 | return ( 8 |
    9 | 10 | 11 | 12 |
    13 |
    14 |

    Contact

    15 |
    16 |
    17 |
    18 |

    Please send feedback to contact@quantecon.org

    19 |
    20 |
    21 | 22 |
    23 | 24 |
    25 | ) 26 | } 27 | } 28 | 29 | export default Contact -------------------------------------------------------------------------------- /client/src/utils/localStorage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Local storage utilities 3 | * @author Trevor Lyon 4 | * 5 | * @module localStorage 6 | */ 7 | 8 | /** 9 | * @function loadState 10 | * @description Loads the JWT from the local storage 11 | */ 12 | export const loadState = () => { 13 | try { 14 | const serializedState = localStorage.getItem('token'); 15 | if(serializedState === null){ 16 | return undefined 17 | } 18 | return JSON.parse(serializedState); 19 | } catch(err){ 20 | console.log('[LocalStorage] - error loading state: ', err); 21 | return undefined; 22 | } 23 | } 24 | 25 | /** 26 | * @function saveState 27 | * @description Saves the state to local storage. State should just contain a JWT 28 | * @param {Object} state State to save in local storage 29 | */ 30 | export const saveState = (state) => { 31 | try { 32 | const serializedState = JSON.stringify(state); 33 | localStorage.setItem('token', serializedState); 34 | } catch(err){ 35 | 36 | } 37 | } -------------------------------------------------------------------------------- /client/src/components/AdminRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route, Redirect} from 'react-router-dom' 3 | import store from '../store/store' 4 | import {getParamByName} from '../utils/url'; 5 | import TempComponent from './TempComponent'; 6 | 7 | export default function AdminRoute({ 8 | component: Component, 9 | ...rest 10 | }) { 11 | const state = store.getState(); 12 | const fromAPI = getParamByName('fromAPI', window.location.href); 13 | return ( 14 | state.auth.isSignedIn === true && state.auth.isAdmin === true 18 | ?
    19 | {fromAPI 20 | ? 21 | :} 22 |
    23 | : }/> 30 | ) 31 | } -------------------------------------------------------------------------------- /client/src/assets/scss/src/helpers/_variables.scss: -------------------------------------------------------------------------------- 1 | // Main 2 | $body-font-family: 'Raleway', sans-serif; // Font-family 3 | 4 | $color-dark: #444444; // Default font colour 5 | $color-medium: #cacaca; // Borders 6 | $color-light: #888888; // Faint font colour 7 | $color-primary: #d84d0a; 8 | $color-secondary: #2473a8; 9 | $color-background: #f1f1f1; 10 | $color-primary-text: #0a0a0a; // Primary text colour 11 | $color-primary-form: #0a0a0a; // Labels in submission 12 | $color-selection-border: #cacaca; // Selection box border 13 | $color-selection-background: #fefefe; // Selection box background 14 | 15 | //Breadcrumb styles 16 | $bc-color-slash: #cacaca; // Breadcrumb slashes betwee links 17 | $bc-font-size: 0.85938rem; // Breadcrumb font size 18 | 19 | //Comments styles 20 | $com-label-size: 1.4rem; // Label right above the comment textbox 21 | 22 | //Header styles 23 | $hd-color-dark: #444; // Background of the header 24 | $hd-color-light: #fff; 25 | $hd-color-secondary: #e43; 26 | $hd-font-family: 'Exo', sans-serif; 27 | 28 | // errors, alerts , warnings 29 | $color-error-comments: #ff425d; -------------------------------------------------------------------------------- /scripts/util/util-input.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ask for input with type & default hints, echos answer. 4 | # (String) What is the hostname: [localhost] 5 | 6 | # Options: 7 | # ./input e.g. (String) Hostname? [localhost] 8 | # ./input --bool e.g. [Y/n] Use Github auth? [n] 9 | 10 | # input 22 | function __input_bool { 23 | read -p "(Y/n) ${1} [${2}] " answer 24 | case ${answer} in 25 | y|Y) 26 | echo true 27 | ;; 28 | *) 29 | echo false 30 | ;; 31 | esac 32 | } 33 | 34 | function input { 35 | case $1 in 36 | --bool) 37 | __input_bool "${2}" "${3}" 38 | ;; 39 | *) 40 | # returnNum $1 41 | __input_string "${1}" "${2}" "${3}" 42 | ;; 43 | esac 44 | } -------------------------------------------------------------------------------- /client/src/store/configure-store.js: -------------------------------------------------------------------------------- 1 | import rootReducer from '../reducers'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | 4 | /* 5 | { 6 | submissionByID: { 7 | *submissionID*: { 8 | isFetching: Boolean, 9 | didInvalidate: Boolean, 10 | lastUpdated: Date, 11 | data: {} 12 | }, 13 | ... 14 | } 15 | submissionList: { 16 | isFetching: Boolean, 17 | didInvalidate: Boolean, 18 | lastUpdated: Date, 19 | previews: [], 20 | authors: [] 21 | }, 22 | userByID: { 23 | *userID*: { 24 | isFetching: Boolean, 25 | didInvalidate: Boolean, 26 | lastUpdated: Date, 27 | data: {} 28 | }, 29 | ... 30 | } 31 | } 32 | */ 33 | 34 | import { 35 | createStore, 36 | compose, 37 | applyMiddleware 38 | } from 'redux'; 39 | 40 | const enhancers = compose(applyMiddleware(thunkMiddleware), window.devToolsExtension ? window.devToolsExtension() : f => f) 41 | 42 | export default (initialState) => { 43 | return createStore(rootReducer, initialState, enhancers); 44 | } -------------------------------------------------------------------------------- /server/js/db/mongoose.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tlyon on 5/26/17. 3 | */ 4 | // modules ====================================================== 5 | var mongoose = require('mongoose'); 6 | var config = require('./_config'); 7 | const maxNumTries = 5 8 | 9 | // config ======================================================= 10 | const options = { 11 | autoReconnect: true, 12 | reconnectTries: 5, 13 | reconnectInterval: 2000 14 | } 15 | 16 | connect = (numTry) => { 17 | if (numTry >= maxNumTries) { 18 | console.error("\nERROR: Couldn't connect to mongo\n"); 19 | } else { 20 | console.log("Connecting...(" + numTry + ")") 21 | mongoose.connect(config.url, options).then( 22 | () => { 23 | console.log("\nConnected to database!\n") 24 | }, 25 | err => { 26 | console.log("\nError connecting to mongo: ", err.message) 27 | console.log("Trying again in 2 seconds...\n") 28 | setTimeout(() => { 29 | connect(numTry+1) 30 | }, 2000); 31 | } 32 | ) 33 | } 34 | } 35 | 36 | connect(0); -------------------------------------------------------------------------------- /server/js/db/migrations/1548822198972-delete-empty-comment.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // modules ================================== 4 | var mongoose = require('mongoose'); 5 | var config = require('./../_config'); 6 | var Comments = require('../models/Comment'); 7 | 8 | // config =================================== 9 | const options = { 10 | autoReconnect: true, 11 | reconnectTries: 5, 12 | reconnectInterval: 2000 13 | } 14 | 15 | module.exports.up = function (next) { 16 | return mongoose.connect(config.url, options).then(db => { 17 | return Comments.findAndModify({ 18 | query: { "content": "" }, 19 | update: { $set: {"deleted": true }}, 20 | upsert: true 21 | }) 22 | }) 23 | .then(() => { 24 | mongoose.close() 25 | return next() 26 | }) 27 | .catch(err => next(err)) 28 | } 29 | 30 | module.exports.down = function (next) { 31 | return mongoose.connect(config.url, options).then(db => { 32 | return Comments.findAndModify({ 33 | query: { "content": "" }, 34 | update: { $set: {"deleted": false }}, 35 | upsert: true 36 | }) 37 | }) 38 | .then(() => { 39 | mongoose.close() 40 | return next() 41 | }) 42 | .catch(err => next(err)) 43 | } -------------------------------------------------------------------------------- /server/js/db/models/Submission.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tlyon on 5/26/17. 3 | */ 4 | 5 | var mongoose = require('mongoose'); 6 | var mongoosePaginate = require('mongoose-paginate'); 7 | var Schema = mongoose.Schema; 8 | var ObjectId = Schema.ObjectId; 9 | 10 | var submissionSchema = new Schema({ 11 | title: String, 12 | topics: [String], 13 | lang: String, 14 | summary: String, 15 | fileName: String, 16 | notebookJSONString: String, 17 | ipynbFile: String, 18 | 19 | author: ObjectId, 20 | authorName: String, 21 | coAuthors: Array, 22 | comments: [ObjectId], 23 | totalComments: Number, 24 | 25 | score: Number, 26 | views: Number, 27 | 28 | published: Date, 29 | lastUpdated: Date, 30 | lastUpdateDate: Date, 31 | 32 | flagged: Boolean, 33 | flaggedReason: String, 34 | deleted: Boolean, 35 | 36 | deletedDate: Date, 37 | 38 | preRendered: Boolean, 39 | views: Number, 40 | viewers: [ObjectId], 41 | viewers_count: Number 42 | }); 43 | 44 | submissionSchema.plugin(mongoosePaginate); 45 | submissionSchema.index({title: 'text', summary: 'text', authorName: 'text'}, {"weights": {title: 2, authorName: 2, summary: 1}}); 46 | 47 | 48 | module.exports = mongoose.model("Submission", submissionSchema); 49 | -------------------------------------------------------------------------------- /scripts/dev-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Small utility script to start the server & client. 4 | # Although this is (currently) a simple process, launching 5 | # may become more complex, and so this is a good starting point. 6 | 7 | # Options 8 | # ./dev-start.sh --client Start client only 9 | # ./dev-start.sh --server Start server only 10 | 11 | # Current process: 12 | # (in root dir) npm start 13 | # (in client dir) npm start 14 | 15 | # Start the server 16 | function start_server { 17 | echo "Starting server..." 18 | cd ../ 19 | npm start 20 | } 21 | 22 | # Start the client 23 | function start_client { 24 | echo "Starting client..." 25 | cd ../client/ 26 | npm start 27 | exit 28 | } 29 | 30 | # Print finish message 31 | function finish { 32 | echo -e "\nStopped." 33 | } 34 | 35 | # Call finish() on ctrl+c (SIGINT) 36 | trap finish INT 37 | 38 | # By default, start both server & client 39 | # (This script calls itself to create 2 separate processes) 40 | if [ $# == 0 ] 41 | then 42 | ./$0 --server & 43 | ./$0 --client 44 | exit 45 | fi 46 | 47 | # Flags for install JUST the server (--server) or client (--client) 48 | case $1 in 49 | "--client") 50 | start_client 51 | ;; 52 | "--server") 53 | start_server 54 | ;; 55 | esac 56 | 57 | exit -------------------------------------------------------------------------------- /server/js/sorting.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to change the order of notebook randomly with a given probability 3 | * 4 | * @param {probability with which you want to swap a notebook} prob 5 | * @param {the present index to operate on} currentIndex 6 | * @param {total number of notebooks} totalNo 7 | * @param {which indexes have been visited and operated upon} visitedArray 8 | * @param {the notebook data} data 9 | */ 10 | var changeOrderRandomly = function (prob, currentIndex, totalNo, visitedArray, data) { 11 | let randomNumber = Math.round(Math.random()*(totalNo - 1)) 12 | if (visitedArray.length < totalNo) { 13 | while (visitedArray.includes(randomNumber)) { 14 | randomNumber = Math.round(Math.random()*(totalNo - 1)) 15 | } 16 | } else { 17 | return; 18 | } 19 | let changeIndex = (Math.random() <= prob) 20 | if (changeIndex) { 21 | let temp = data[randomNumber] 22 | data[randomNumber] = data[currentIndex] 23 | data[currentIndex] = temp; 24 | visitedArray.push(randomNumber); 25 | visitedArray.push(currentIndex); 26 | } else { 27 | if (!visitedArray.includes(currentIndex)) { 28 | visitedArray.push(currentIndex) 29 | } 30 | } 31 | } 32 | 33 | module.exports = { 34 | changeOrderRandomly, 35 | }; -------------------------------------------------------------------------------- /scripts/dev-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Small utility script to install server & client NPM modules. 4 | # Although this is (currently) a simple process, installation 5 | # may become more complex, and so this is a good starting point. 6 | 7 | # Options 8 | # ./dev-install.sh --client Install client only 9 | # ./dev-install.sh --server Install server only 10 | 11 | # Current process: 12 | # (in root dir) npm install 13 | # (in client dir) npm install 14 | 15 | # Install node modules in root directory 16 | function install_server { 17 | echo "Installing server..." 18 | cd ../ 19 | npm install --quiet 20 | cd scripts/ 21 | echo "Finished installing server." 22 | } 23 | 24 | # Install node modules in /client subdirectory 25 | function install_client { 26 | echo "Installing client..." 27 | cd ../ 28 | cd client 29 | npm install --quiet 30 | cd ../scripts/ 31 | echo "Finished installing client." 32 | } 33 | 34 | # By default, install both server & client 35 | if [ $# == 0 ] 36 | then 37 | install_server 38 | install_client 39 | exit 40 | fi 41 | 42 | # Flags for install JUST the server (--server) or client (--client) 43 | case $1 in 44 | "--client") 45 | install_client 46 | ;; 47 | "--server") 48 | install_server 49 | ;; 50 | esac 51 | 52 | exit -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QuantEcon – Notes 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 |
    34 |
    35 |
    36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /client/src/reducers/User.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_USER_INFO, 3 | RECEIVE_USER_INFO, 4 | REQUIRE_SIGN_IN 5 | } from '../actions/user' 6 | 7 | const UserReducer = (state = {}, action) => { 8 | switch(action.type){ 9 | case REQUEST_USER_INFO: 10 | var newState = Object.assign({}, state, { 11 | isFetching: true, 12 | [action.userID]: { 13 | isFetching: true, 14 | didInvalidate: false 15 | } 16 | }); 17 | return newState; 18 | case RECEIVE_USER_INFO: 19 | var receiveNewState = Object.assign({}, state, { 20 | isFetching: false, 21 | [action.userID]: { 22 | isFetching: false, 23 | didInvalidate: false, 24 | lastUpdated: action.receivedAt, 25 | data: action.data 26 | } 27 | }) 28 | return receiveNewState; 29 | case REQUIRE_SIGN_IN: 30 | return Object.assign({}, state, { 31 | isFetching: false, 32 | [action.userID]: { 33 | isFetching: false, 34 | didInvalidate: false 35 | }, 36 | }) 37 | default: 38 | return state 39 | } 40 | } 41 | 42 | export default UserReducer; -------------------------------------------------------------------------------- /client/src/components/submissions/EditSubmission.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Submit from '../submit/Submit' 4 | 5 | /** 6 | * Componnet that renders the {@link Submit} component in an edit submission environment. 7 | * 8 | * It's parent component, {@link EditSubmissionContainer} passes all necessary data to this. 9 | */ 10 | class EditSubmission extends Component { 11 | /** 12 | * @prop {Object} currentUser Object containing the current user's information. If no 13 | * user is signed in, will be `null` 14 | * @prop {Object} submission Object containing all the information of the submission 15 | * @prop {Function} save Method 16 | */ 17 | static propTypes = { 18 | submission: PropTypes.object.isRequired, 19 | currentUser: PropTypes.object.isRequired, 20 | save: PropTypes.func.isRequired, 21 | history: PropTypes.object.isRequired 22 | } 23 | 24 | render() { 25 | return ( 26 |
    27 | 33 |
    34 | ) 35 | } 36 | } 37 | 38 | export default EditSubmission -------------------------------------------------------------------------------- /client/src/assets/scss/src/layouts/admin.scss: -------------------------------------------------------------------------------- 1 | .admin-content { 2 | padding: 1rem; 3 | margin: 1rem; 4 | .section-header { 5 | button { 6 | @include medium-button; 7 | display: inline-block; 8 | background:#fff; 9 | color: $color-primary; 10 | margin:0 1rem 0 0; 11 | cursor: pointer; 12 | } 13 | } 14 | } 15 | 16 | .admin-button-row { 17 | list-style: none; 18 | padding:0; 19 | margin:1rem 0; 20 | display: flex; 21 | li { 22 | margin:0 1rem 0 0; 23 | button { 24 | @include medium-button; 25 | color: $color-primary; 26 | cursor: pointer; 27 | } 28 | } 29 | } 30 | 31 | .page-title .announcements .container { 32 | width: auto; 33 | text-align: center; 34 | } 35 | 36 | .announcements { 37 | button { 38 | @include medium-button; 39 | display: inline-block; 40 | background:#fff; 41 | color: $color-primary; 42 | margin:1rem 1rem 0 0; 43 | cursor: pointer; 44 | } 45 | } 46 | 47 | .announcements-list { 48 | text-align: left; 49 | border: 1px solid #c3c3c3; 50 | background: #dee7db; 51 | padding: 1rem 2rem; 52 | border-radius: 5px; 53 | margin: 1rem 0; 54 | .announcement { 55 | border-bottom: 1px solid #c3c3c3; 56 | font-weight: 700; 57 | &:last-child { 58 | border-bottom:0; 59 | } 60 | } 61 | } 62 | 63 | 64 | .admin-tag { 65 | text-align: left; 66 | margin: 10px; 67 | } 68 | -------------------------------------------------------------------------------- /server/js/auth/jwt.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const JwtStrategy = require('passport-jwt').Strategy; 3 | const ExtractJwt = require('passport-jwt').ExtractJwt; 4 | const _config = require('../../_config'); 5 | const User = require('../db/models/User') 6 | 7 | var opts = { 8 | jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderWithScheme('jwt'), ExtractJwt.fromUrlQueryParameter('jwt')]), 9 | secretOrKey: "banana horse laser muffin" 10 | } 11 | 12 | const select = 'name views numComments joinDate voteScore position submissions upvotes downvotes' + 13 | ' avatar website email summary activeAvatar currentProvider github fb twitter google oneSocial emailSettings new' 14 | 15 | passport.use(new JwtStrategy(opts, function (jwt_payload, done) { 16 | console.log("[JWTStrategy]JWT Payload: ", jwt_payload) 17 | User.findOne({ 18 | _id: jwt_payload.user._id 19 | }, select, function (err, user) { 20 | if (err) { 21 | console.log('[JWTStrategy] - error finding user:', err); 22 | return done(err, false) 23 | } else if (user) { 24 | // console.log('[JWTStrategy] - found user: ', user); 25 | done(null, user, {isAdmin: jwt_payload.isAdmin}); 26 | } else { 27 | console.log('[JWTStrategy] - no user'); 28 | done(null, false); 29 | } 30 | }) 31 | })); 32 | 33 | module.exports = passport; -------------------------------------------------------------------------------- /client/src/components/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import HeadContainer from "../containers/HeadContainer"; 3 | import Breadcrumbs from './partials/Breadcrumbs' 4 | 5 | class NotFound extends Component { 6 | render() { 7 | return ( 8 |
    9 | 10 | 11 | 12 |
    13 |
    14 |

    Page Not Found

    15 |
    16 |
    17 |
    18 | 19 |

    We couldn’t find the page you were looking for.

    20 | 21 |

    Please check the URL or try a link below:

    22 | 23 | 30 | 31 |
    32 |
    33 | 34 |
    35 | 36 |
    37 | ) 38 | } 39 | } 40 | 41 | export default NotFound; -------------------------------------------------------------------------------- /client/src/components/partials/Notebook.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, Fragment} from 'react'; 2 | import NotebookFromHTML from '../NotebookFromHTML'; 3 | import NotebookPreview from '@nteract/notebook-preview' 4 | 5 | class Notebook extends Component { 6 | render() { 7 | return ( 8 | 9 | {this.props.nbLoading 10 | ?
    11 | {/* TODO: display download progress */} 12 | 13 |
    14 | Loading... ({this.props.dataReceived} / {this.props.totalData}) 15 |
    16 | 17 |
    18 |
    : 19 | {this.props.html 20 | ?
    21 |

    (pre-rendered notebook)

    22 | 23 |
    24 | :
    25 | 26 |
    } 27 |
    28 | } 29 |
    30 | ) 31 | } 32 | } 33 | 34 | export default Notebook; -------------------------------------------------------------------------------- /client/src/containers/AnnouncementsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux' 3 | import {bindActionCreators} from 'redux' 4 | import {editRecent, fetchRecent, deleteRecent} from '../actions/announcements' 5 | 6 | import AnnouncementsBanner from '../components/partials/AnnouncementsBanner' 7 | 8 | class AnnouncementsContainer extends Component { 9 | 10 | constructor(props) { 11 | super(props) 12 | 13 | props.actions.fetchRecent() 14 | } 15 | 16 | render() { 17 | return ( 18 |
    19 | {this.props.announcement || this.props.showAdmin 20 | ? 25 | : null} 26 | 27 |
    28 | 29 | ) 30 | } 31 | } 32 | 33 | function mapStateToProps(state, props) { 34 | return { 35 | announcement: state.announcements.recent 36 | } 37 | } 38 | 39 | function mapDispatchToProps(dispatch) { 40 | return { 41 | actions: bindActionCreators({ 42 | fetchRecent, 43 | editRecent, 44 | deleteRecent 45 | }, dispatch) 46 | } 47 | } 48 | 49 | export default connect(mapStateToProps, mapDispatchToProps)(AnnouncementsContainer); -------------------------------------------------------------------------------- /client/src/containers/user/MyProfileContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import User from '../../components/user/User' 3 | import {connect} from 'react-redux' 4 | import {bindActionCreators} from 'redux' 5 | import * as AuthActions from '../../actions/auth/signOut' 6 | 7 | class MyProfileContainer extends Component { 8 | componentWillReceiveProps(props){ 9 | if(!props.loading && !props.isSignedIn){ 10 | props.history.replace('/') 11 | } 12 | } 13 | 14 | render() { 15 | console.log('[USER Container]', this.props) 16 | return ( 17 |
    18 | {this.props.loading 19 | ? "loading..." 20 | : } 26 |
    27 | ) 28 | } 29 | } 30 | 31 | const mapStateToProps = (state, props) => { 32 | return { 33 | user: state.auth.user, 34 | loading: state.auth.loading, 35 | isSignedIn: state.auth.isSignedIn 36 | 37 | } 38 | } 39 | 40 | const mapDispatchToProps = (dispatch) => { 41 | return { 42 | actions: bindActionCreators(AuthActions, dispatch) 43 | } 44 | } 45 | 46 | export default connect(mapStateToProps, mapDispatchToProps)(MyProfileContainer); 47 | -------------------------------------------------------------------------------- /client/src/containers/user/UserContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import User from '../../components/user/User'; 3 | import {connect} from 'react-redux' 4 | import {bindActionCreators} from 'redux' 5 | import * as UserActions from '../../actions/user' 6 | 7 | class UserContainer extends Component { 8 | constructor(props){ 9 | super(props); 10 | 11 | this.props.actions.fetchUserInfo(props.match.params.userID); 12 | } 13 | 14 | render(){ 15 | return( 16 |
    17 | 22 |
    23 | ) 24 | } 25 | } 26 | 27 | const mapStateToProps = (state, props) => { 28 | 29 | var il = true; 30 | if(state.userByID[props.match.params.userID]){ 31 | il = state.userByID[props.match.params.userID].isFetching; 32 | } 33 | return { 34 | user: state.userByID[props.match.params.userID], 35 | isLoading: il, 36 | isAdmin: state.auth.isAdmin 37 | } 38 | } 39 | 40 | const mapDispatchToProps = (dispatch) => { 41 | return { 42 | actions: bindActionCreators(UserActions, dispatch) 43 | } 44 | } 45 | 46 | export default connect(mapStateToProps, mapDispatchToProps)(UserContainer); -------------------------------------------------------------------------------- /client/src/components/comments/Reply.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types' 3 | 4 | import CommentContainer from "../../containers/comment/CommentContainer"; 5 | 6 | /** 7 | * Renders a reply to a comment. Wraps a {@link Comment} in a reply div. 8 | * 9 | * Becuase replies to a reply is not available, passing `isReply: true` to the {@link Comment} 10 | * as a prop disables this functionality. 11 | */ 12 | class Reply extends Component { 13 | /** 14 | * @prop {Object} reply Comment object containing all data for the reply 15 | * @prop {Object} author User object containing all data for the author of the reply 16 | */ 17 | static propTypes = { 18 | reply: PropTypes.object.isRequired, 19 | author: PropTypes.object.isRequired 20 | } 21 | constructor(props) { 22 | super(props) 23 | 24 | this.state = { 25 | reply: props.reply, 26 | author: props.author 27 | } 28 | } 29 | 30 | render() { 31 | return ( 32 |
    33 | 39 |
    40 | ) 41 | } 42 | } 43 | 44 | export default Reply; 45 | -------------------------------------------------------------------------------- /client/src/actions/auth/signOut.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Sign out actions 3 | * @author Trevor Lyon 4 | * 5 | * @module signOutActions 6 | */ 7 | import axios from 'axios' 8 | import store from '../../store/store'; 9 | 10 | export const SIGN_OUT = 'SIGN_OUT' 11 | const signOutAction = ({ 12 | error 13 | }) => { 14 | return { 15 | type: SIGN_OUT, 16 | error 17 | } 18 | } 19 | 20 | /** 21 | * @function signOut 22 | * @description 23 | * Makes an api request to invalidate the token. Then removes the token from local storage 24 | * and sets the `currentUser` in the redux store to `null` 25 | */ 26 | export const signOut = () => { 27 | return function (dispatch) { 28 | if (store.getState().auth.isSignedIn) { 29 | axios.get('/api/auth/sign-out', { 30 | headers: { 31 | 'authorization': 'JWT ' + store.getState().auth.token 32 | } 33 | }).then(resp => { 34 | if(resp.data.error){ 35 | console.log('[AuthActions] - error signing out: ', resp.data.error); 36 | dispatch(signOutAction({error: resp.data.error})); 37 | } else { 38 | dispatch(signOutAction()); 39 | } 40 | }).catch(error => { 41 | dispatch(signOutAction({error})); 42 | }) 43 | } else { 44 | dispatch(signOutAction({ 45 | error: 'Not signed in' 46 | })) 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /client/src/containers/auth/OAuthSignInButton.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | import {connect} from 'react-redux' 4 | import {bindActionCreators} from 'redux' 5 | import * as AuthActions from '../../actions/auth/signIn' 6 | 7 | import SignInButton from '../../components/auth/SignInButton'; 8 | 9 | class OAuthSignInButton extends Component { 10 | constructor(props) { 11 | super(props); 12 | // console.log('[OAuthSignInButton] - props:', props); 13 | this.signIn = this 14 | .signIn 15 | .bind(this); 16 | } 17 | 18 | signIn() { 19 | console.log('[OAuthSignInButton] - provider: ', this.props.provider); 20 | if (this.props.isAdd) { 21 | this 22 | .props 23 | .actions 24 | .addSocial(this.props.provider, this.props.next); 25 | } else { 26 | this 27 | .props 28 | .actions 29 | .signIn(this.props.provider, this.props.next); 30 | } 31 | 32 | } 33 | 34 | render() { 35 | return () 41 | } 42 | } 43 | 44 | function mapDispathToProps(dispatch) { 45 | return { 46 | actions: bindActionCreators(AuthActions, dispatch) 47 | } 48 | } 49 | 50 | export default connect(null, mapDispathToProps)(OAuthSignInButton); -------------------------------------------------------------------------------- /client/src/assets/scss/src/components/pagination.scss: -------------------------------------------------------------------------------- 1 | // Pagination styles 2 | 3 | .pagination { 4 | margin: 0 0 1rem 0; 5 | border-top:1px solid #e5e5e5; 6 | padding: 2rem 0 0 0; 7 | text-align: center; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | list-style: none; 12 | li { 13 | margin: 0 0.2rem; 14 | padding:0; 15 | } 16 | a, button { 17 | display: block; 18 | padding: 0.23438rem 0.78125rem; 19 | border-radius: 0; 20 | color: #0a0a0a; 21 | } 22 | a:hover, button:hover { 23 | color: $color-primary; 24 | } 25 | .current a { 26 | border:1px solid $color-primary; 27 | background: transparent; 28 | color: $color-primary; 29 | font-weight: bold; 30 | border-radius: 5px; 31 | } 32 | .disabled { 33 | display: none; 34 | color: #888; 35 | cursor: not-allowed; 36 | &:hover { 37 | background: transparent; 38 | } 39 | } 40 | .ellipsis::after { 41 | padding: 0.23438rem 0.78125rem; 42 | content: '\2026'; 43 | color: #0a0a0a; 44 | } 45 | } 46 | 47 | @media print, screen and (min-width: 40em) { 48 | .pagination li { 49 | display: inline-block; 50 | } 51 | } 52 | 53 | .pagination-previous { 54 | a::before { 55 | display: inline-block; 56 | margin-right: 0.5rem; 57 | content: '\00ab'; 58 | } 59 | } 60 | 61 | .pagination-next { 62 | a::after { 63 | display: inline-block; 64 | margin-left: 0.5rem; 65 | content: '\00bb'; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /client/src/components/About.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import HeadContainer from '../containers/HeadContainer'; 3 | import Breadcrumbs from './partials/Breadcrumbs' 4 | import Markdown from 'react-markdown' 5 | import axios from 'axios' 6 | 7 | class About extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | content: 'loading...' 12 | } 13 | console.log('[About] - fetching about page'); 14 | axios 15 | .get('/api/about') 16 | .then(resp => { 17 | console.log('[About] - returned resp: ', resp); 18 | this.setState({content: resp.data.content}) 19 | }) 20 | .catch(err => { 21 | console.log('[About] - err:', err); 22 | this.setState({error: err}) 23 | }) 24 | } 25 | 26 | render() { 27 | return ( 28 |
    29 | 30 | 31 |
    32 |
    33 |

    34 | About 35 |

    36 |
    37 | 38 |
    39 | 40 |
    41 |
    42 |
    43 | ) 44 | } 45 | } 46 | export default About; -------------------------------------------------------------------------------- /client/src/assets/scss/src/main.scss: -------------------------------------------------------------------------------- 1 | @import './helpers/_variables.scss'; // Global variables 2 | 3 | @import './libraries/normalize.scss'; // normalize.css v7.0.0 4 | @import './libraries/html5boilerplate.scss'; // HTML5 Boilerplate v6.0.1 5 | @import './helpers/_fonts.scss'; // Load Google Fonts 6 | @import './helpers/_foundation-icons.scss'; // Foundation Icons v 3.0 7 | @import './helpers/_qe-menubar.scss'; // QuantEcon menubar styles 8 | @import './helpers/_settings.scss'; // Default element styles, variables, mixins 9 | @import './layouts/header.scss'; // Template header styles 10 | @import './layouts/footer.scss'; // Template footer styles 11 | @import './layouts/listing.scss'; // Submission listing styles 12 | @import './layouts/content-details.scss'; // Layout styles for submission and user landing pages 13 | @import './components/forms.scss'; // Submission and edit profile form styles 14 | @import './components/breadcrumbs.scss'; // Breadcrumb styles 15 | @import './components/pagination.scss'; // Pagination styles 16 | @import './components/modals.scss'; // Modal styles 17 | @import './libraries/dropzone.scss'; // Dropzone file submission upload styles 18 | @import './components/comments.scss'; // Submission commenting styles 19 | @import './components/submit.scss'; // Submission styles 20 | 21 | //@import 'notebook-render.scss'; // Notebook rendering styles 22 | @import './layouts/admin.scss'; // Admin area styles 23 | @import './helpers/_mobile.scss'; // Mobile override styles 24 | @import 'misc.scss'; // Miscellaneous styles 25 | @import './partials/buttons.scss'; // Button styles 26 | -------------------------------------------------------------------------------- /server/assets/nbconvert/templates/notebookHTML.tpl: -------------------------------------------------------------------------------- 1 | {% extends 'full.tpl'%} 2 | {%- block html_head -%} 3 | 4 | 5 | {%- if "widgets" in nb.metadata -%} 6 | 7 | {%- endif-%} 8 | 9 | 10 | 11 | 12 | {% for css in resources.inlining.css -%} 13 | 16 | {% endfor %} 17 | 18 | 51 | 52 | 53 | 54 | 55 | 56 | {{ mathjax() }} 57 | {%- endblock html_head -%} -------------------------------------------------------------------------------- /server/js/languages.js: -------------------------------------------------------------------------------- 1 | const Submission = require('../js/db/models/Submission'); 2 | 3 | /** 4 | * Find all the currently available meta languages in all submissions 5 | * @return {Promise} returns promise of meta language and total length of submissions 6 | */ 7 | const metaLanguages = () => { 8 | const availableLanguages = []; 9 | return new Promise((resolve, reject) => { 10 | Submission.find({'deleted': false}, (err, submissions) => { // find submissions that have not been archived. 11 | if(err) { 12 | console.log('Error occurred finding deleted submissions', err); 13 | reject(err); 14 | } 15 | return submissions; 16 | }).then((submissions) => { 17 | // save currently available languages from unarchived notebooks 18 | submissions.map((submission) => { 19 | if(!availableLanguages.includes(submission.lang)) { 20 | availableLanguages.push(submission.lang); 21 | } 22 | }) 23 | return {availableLanguages: availableLanguages, totalSubmissions: submissions.length}; 24 | }).then((result) => { 25 | resolve(result); 26 | }); 27 | }) 28 | }; 29 | 30 | /** 31 | * Returns promise from metaLanguage 32 | * 33 | * @param {Object} submissions all submissions that have not been archieved from database 34 | * 35 | * @return {Promise} returns promise of meta languages and total length of submissions 36 | */ 37 | const resolveLanguagePromise = async (submissions) => { 38 | const result = await metaLanguages(submissions); 39 | return result; 40 | }; 41 | 42 | module.exports = { 43 | metaLanguages, 44 | resolveLanguagePromise 45 | }; -------------------------------------------------------------------------------- /server/js/db/migrations/1555396627360-resize-avatars-to-50px-google.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // modules ================================== 4 | let mongoose = require('mongoose'); 5 | let config = require('./../_config'); 6 | let Users = require('../models/User'); 7 | 8 | // config =================================== 9 | const options = { 10 | autoReconnect: true, 11 | reconnectTries: 5, 12 | reconnectInterval: 2000 13 | } 14 | 15 | module.exports.up = function (next) { 16 | return mongoose.connect(config.url, options).then(db => { 17 | return Users.find({ $and: [{ currentProvider: "Google" }, { avatar: { $regex: /^((?!sz=50).)*$/ } }] }, function(err, docs) { 18 | for (let e in docs) { 19 | let doc = docs[e]; 20 | doc.avatar= doc.avatar + '?sz=50'; 21 | doc.save((err) => { 22 | if (err) { 23 | console.log(err); 24 | } else { 25 | } 26 | }) 27 | } 28 | }) 29 | }) 30 | .then(() => { 31 | mongoose.close() 32 | return next() 33 | }) 34 | .catch(err => next(err)) 35 | } 36 | 37 | module.exports.down = function (next) { 38 | return mongoose.connect(config.url, options).then(db => { 39 | return Users.find({ $and: [{ currentProvider: "Google" }, { avatar: { $regex: /^((?sz=50).)*$/ } }] }, function(err, docs) { 40 | for (let e in docs) { 41 | let doc = docs[e]; 42 | doc.avatar= doc.avatar.replace('?sz=50',''); 43 | doc.save((err) => { 44 | if (err) { 45 | console.log(err); 46 | } else { 47 | } 48 | }) 49 | } 50 | }) 51 | }) 52 | .then(() => { 53 | mongoose.close() 54 | return next() 55 | }) 56 | .catch(err => next(err)) 57 | } -------------------------------------------------------------------------------- /server/js/db/migrations/1555396634463-resize-avatars-to-50px-github.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // modules ================================== 4 | let mongoose = require('mongoose'); 5 | let config = require('./../_config'); 6 | let Users = require('../models/User'); 7 | 8 | // config =================================== 9 | const options = { 10 | autoReconnect: true, 11 | reconnectTries: 5, 12 | reconnectInterval: 2000 13 | } 14 | 15 | module.exports.up = function (next) { 16 | return mongoose.connect(config.url, options).then(db => { 17 | return Users.find({ $and: [{ currentProvider: "Github" }, { avatar: { $regex: /^.*[^s=75|^s=50]$/ } }] }, function(err, docs) { 18 | for (let e in docs) { 19 | let doc = docs[e]; 20 | doc.avatar = doc.avatar + '&s=50'; 21 | doc.save((err) => { 22 | if (err) { 23 | console.log(err); 24 | } else { 25 | } 26 | }) 27 | } 28 | }); 29 | }) 30 | .then(() => { 31 | mongoose.close() 32 | return next() 33 | }) 34 | .catch(err => next(err)) 35 | } 36 | 37 | module.exports.down = function (next) { 38 | return mongoose.connect(config.url, options).then(db => { 39 | return Users.find({ $and: [{ currentProvider: "Github" }, { avatar: { $regex: /^.*[s=50]$/ } }] }, function(err, docs) { 40 | for (let e in docs) { 41 | let doc = docs[e]; 42 | doc.avatar = doc.avatar.replace('&s=50',''); 43 | doc.save((err) => { 44 | if (err) { 45 | console.log(err); 46 | } else { 47 | } 48 | }) 49 | } 50 | }); 51 | }) 52 | .then(() => { 53 | mongoose.close() 54 | return next() 55 | }) 56 | .catch(err => next(err)) 57 | } -------------------------------------------------------------------------------- /client/src/components/comments/ReplyList.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | import Reply from './Reply'; 4 | 5 | /** 6 | * Simliar to {@link CommentsThread} but used for replies. Renders all 7 | * replies to comments 8 | * 9 | * Children: {@link Reply} 10 | */ 11 | class ReplyList extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | replies: props.replies, 17 | authors: props.authors 18 | } 19 | 20 | } 21 | 22 | componentWillReceiveProps(props) { 23 | console.log('[ReplyList] - will receive props: ', props); 24 | this.setState({replies: props.replies, authors: props.authors}) 25 | } 26 | render() { 27 | return ( 28 |
    29 | {/*Render each reply*/} 30 | {this.state.replies.map((reply, index) => { 31 | if (reply && !reply.deleted) { 32 | var author = this 33 | .state 34 | .authors 35 | .filter(function (user) { 36 | return user._id === reply.author; 37 | }); 38 | return 44 | } else { 45 | return null; 46 | } 47 | })} 48 |
    49 | ) 50 | } 51 | } 52 | 53 | export default ReplyList; -------------------------------------------------------------------------------- /client/src/containers/SubmitContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux' 3 | import {bindActionCreators} from 'redux' 4 | import Submit from '../components/submit/Submit' 5 | import * as SubmitActions from '../actions/submit' 6 | 7 | /** 8 | * Parent container for {@link Submit}. Retrives the current user from the redux store, 9 | * retrieves the Submit Actions and passes them to the child component 10 | */ 11 | class SubmitContainer extends Component { 12 | constructor(props){ 13 | super(props) 14 | 15 | this.onSubmit = this.onSubmit.bind(this) 16 | } 17 | 18 | /** 19 | * Dispatches a submit action. Called by the {@link Submit} component when the user clicks 20 | * the submit button 21 | * @param {Object} formData All data the user entered in the submission form 22 | * @param {File} file File the user uploaded for the submission 23 | */ 24 | onSubmit (formData, file){ 25 | console.log('[SubmitContainer] - onsubmit: ', formData, file) 26 | this 27 | .props 28 | .actions 29 | .submit(formData, file); 30 | this.props.history.push('/') 31 | } 32 | 33 | render() { 34 | return ( 35 |
    36 | 37 |
    38 | ) 39 | } 40 | } 41 | 42 | function mapStateToProps(state, props) { 43 | return {user: state.auth.user} 44 | } 45 | 46 | function mapDispatchToProps(dispatch) { 47 | console.log("map dispatch to props: ", SubmitActions); 48 | return { 49 | actions: bindActionCreators(SubmitActions, dispatch) 50 | } 51 | } 52 | 53 | export default connect(mapStateToProps, mapDispatchToProps)(SubmitContainer); -------------------------------------------------------------------------------- /client/src/actions/utils.js: -------------------------------------------------------------------------------- 1 | export const LOG_REQUEST_SUBMISSION_START = 'LOG_REQUEST_SUBMISSION_START' 2 | export const logRequestSubmissionStartAction = ({ 3 | submissionID, 4 | id 5 | }) => { 6 | return { 7 | type: LOG_REQUEST_SUBMISSION_START, 8 | submissionID, 9 | time: Date.now(), 10 | id 11 | } 12 | } 13 | 14 | export const logRequestSubmissionStart = ({ 15 | request 16 | }) => { 17 | console.log('log request submission start'); 18 | return (dispatch) => { 19 | dispatch(logRequestSubmissionStartAction({ 20 | submissionID: request.submissionID, 21 | id: request.id 22 | })) 23 | 24 | } 25 | } 26 | 27 | export const LOG_REQUEST_SUBMISSION_END = 'LOG_REQUEST_SUBMISSION_END' 28 | export const logRequestSubmissionEndAction = ({ 29 | submissionID, 30 | size, 31 | id 32 | }) => { 33 | return { 34 | type: LOG_REQUEST_SUBMISSION_END, 35 | submissionID, 36 | time: Date.now(), 37 | size, 38 | id 39 | } 40 | } 41 | 42 | export const logRequestSubmissionEnd = ({ 43 | request 44 | }) => { 45 | return (dispatch) => { 46 | dispatch(logRequestSubmissionEndAction({ 47 | submissionID: request.submissionID, 48 | id: request.id 49 | })) 50 | 51 | } 52 | } 53 | 54 | export const LOG_RENDER_START = 'LOG_RENDER_START' 55 | export const logRenderStartAction = ({request}) => { 56 | return { 57 | type: LOG_RENDER_START, 58 | submissionID: request.submissionID, 59 | id: request.id 60 | } 61 | } 62 | 63 | export const LOG_RENDER_END = 'LOG_RENDER_END' 64 | export const logRenderEndAction = ({request}) => { 65 | return { 66 | type: LOG_RENDER_END, 67 | submissionID: request.submissionID, 68 | id: request.id 69 | } 70 | } -------------------------------------------------------------------------------- /client/src/remark-math/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "remark-math@1.0.3", 5 | "/Users/tlyon3/Work/quantecon/repos/Bookshelf/client" 6 | ] 7 | ], 8 | "_from": "remark-math@1.0.3", 9 | "_id": "remark-math@1.0.3", 10 | "_inBundle": false, 11 | "_integrity": "sha512-kr/gqqrkwN56QbKFNXkCi13TlKvfxiPTLSrsu2jAiIfp8kMMF5KOkyvwvtwtFTyVsbD+WfqbfaadelJKsLfeeA==", 12 | "_location": "/remark-math", 13 | "_phantomChildren": {}, 14 | "_requested": { 15 | "type": "version", 16 | "registry": true, 17 | "raw": "remark-math@1.0.3", 18 | "name": "remark-math", 19 | "escapedName": "remark-math", 20 | "rawSpec": "1.0.3", 21 | "saveSpec": null, 22 | "fetchSpec": "1.0.3" 23 | }, 24 | "_requiredBy": [ 25 | "/" 26 | ], 27 | "_resolved": "https://registry.npmjs.org/remark-math/-/remark-math-1.0.3.tgz", 28 | "_spec": "1.0.3", 29 | "_where": "/Users/tlyon3/Work/quantecon/repos/Bookshelf/client", 30 | "author": { 31 | "name": "Junyoung Choi", 32 | "email": "fluke8259@gmail.com" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/Rokt33r/remark-math/issues" 36 | }, 37 | "dependencies": { 38 | "trim-trailing-lines": "^1.1.0" 39 | }, 40 | "description": "Math Inline and Block parser plugin for Remark", 41 | "files": [ 42 | "index.js", 43 | "block.js", 44 | "inline.js" 45 | ], 46 | "homepage": "https://github.com/Rokt33r/remark-math#readme", 47 | "keywords": [ 48 | "markdown", 49 | "remark", 50 | "math", 51 | "katex", 52 | "latex", 53 | "tex" 54 | ], 55 | "license": "MIT", 56 | "main": "index.js", 57 | "name": "remark-math", 58 | "peerDependencies": { 59 | "remark-parse": "^3.0.0 || ^4.0.0" 60 | }, 61 | "repository": { 62 | "type": "git", 63 | "url": "git+https://github.com/Rokt33r/remark-math.git" 64 | }, 65 | "version": "1.0.3" 66 | } 67 | -------------------------------------------------------------------------------- /client/src/utils/popup.js: -------------------------------------------------------------------------------- 1 | const settings = "scrollbars=no,toolbar=no,location=no,titlebar=no,directories=no,status=no,menubar=no"; 2 | 3 | const getPopupSize = (provider) => { 4 | switch (provider) { 5 | case "facebook": 6 | return { 7 | width: 580, 8 | height: 400 9 | }; 10 | case "google": 11 | return { 12 | width: 452, 13 | height: 633 14 | }; 15 | case "github": 16 | return { 17 | width: 1020, 18 | height: 618 19 | }; 20 | case "twitter": 21 | return { 22 | width: 495, 23 | height: 645 24 | }; 25 | default: 26 | return { 27 | width: 1024, 28 | height: 618 29 | } 30 | } 31 | } 32 | 33 | const getPopupOffset = ({ 34 | width, 35 | height 36 | }) => { 37 | var windowLeft = window.screenLeft ? window.screenLeft : window.screenX 38 | var windowTop = window.screenTop ? window.screenTop : window.screenY 39 | 40 | const left = windowLeft + (window.innerWidth / 2) - (width / 2); 41 | const top = windowTop + (window.innerHeight / 2) - (height / 2) 42 | 43 | return { 44 | left, 45 | top 46 | } 47 | } 48 | 49 | const getPopupDimensions = (provider) => { 50 | const { 51 | width, 52 | height 53 | } = getPopupSize(provider); 54 | const { 55 | top, 56 | left 57 | } = getPopupOffset({ 58 | width, 59 | height 60 | }); 61 | 62 | return `width=${width},height=${height},top=${top},left=${left}`; 63 | } 64 | 65 | const openPopup = (provider, url, name) => { 66 | return window.open(url, name, `${settings},${getPopupDimensions(provider)}`) 67 | } 68 | 69 | export default openPopup; -------------------------------------------------------------------------------- /client/src/components/user/UserPreview.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import Markdown from 'react-markdown'; 4 | 5 | class UserPreview extends Component { 6 | constructor(props){ 7 | super(props) 8 | this.state = { 9 | user: props.user 10 | } 11 | } 12 | 13 | render() { 14 | return ( 15 |
    16 |
    17 |

    18 | 19 | {this.props.user.name} 20 | 21 |

    22 | 28 |
    29 |

    30 | 31 | User's avatar 32 | 33 |

    34 |
    35 |
      36 |
    • 37 | 38 | {this.props.user.submissions 39 | ? this.props.user.submissions.length 40 | : 0} 41 | 42 | Submissions 43 |
    • 44 |
    45 |
    46 |
    47 | ) 48 | } 49 | } 50 | 51 | export default UserPreview -------------------------------------------------------------------------------- /server/js/render.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tlyon on 5/18/17. 3 | */ 4 | 5 | var sprintf = require('sprintf'); 6 | var exec = require('child_process').exec; 7 | var tmp = require('tmp'); 8 | var fs = require('fs'); 9 | var config = require('../_config'); 10 | 11 | function renderHTMLFromFile(file, id){ 12 | var outputDir = config.rootDirectory + config.filesDirectory 13 | var outputName = id; 14 | var templateLocation = '../assets/nbconvert/templates/notebookHTML.tpl' 15 | 16 | var command = sprintf('jupyter nbconvert %s --output=%s --output-dir=%s --template=%s', file.path, outputName, outputDir, templateLocation); 17 | 18 | exec(command, { 19 | maxBuffer: 1024*500 20 | }, (err, stdout, stderr) => { 21 | if(err){ 22 | return {error: err} 23 | } 24 | }) 25 | } 26 | 27 | function renderHTMLFromJSON(json, id){ 28 | var outputDir = config.rootDirectory + config.filesDirectory 29 | var outputName = id; 30 | var templateLocation = config.rootDirectory + '/assets/nbconvert/templates/notebookHTML.tpl' 31 | 32 | var tmpObj = tmp.fileSync({ 33 | mode: 0644, 34 | prefix: 'prefix-', 35 | postfix: '.json' 36 | }); 37 | 38 | console.log('\ttmpObj: ', tmpObj); 39 | console.log('\tOutput dir: ', outputDir); 40 | 41 | fs.writeFileSync(tmpObj.name, json); 42 | 43 | var command = sprintf('jupyter nbconvert %s --output=%s --output-dir=%s --template=%s', tmpObj.name, outputName, outputDir, templateLocation); 44 | 45 | console.log('\tExecute command'); 46 | exec(command, { 47 | maxBuffer: 1024*500 48 | }, (err, stdout, stderr) => { 49 | if(err){ 50 | console.log('\tError executing command: ', err); 51 | return {error: err} 52 | } else { 53 | console.log('\tNo error executing command'); 54 | } 55 | }) 56 | } 57 | 58 | module.exports = {renderHTMLFromFile, renderHTMLFromJSON} -------------------------------------------------------------------------------- /server/routes/restore/restore.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const appConfig = require('../../_config') 3 | const bodyParser = require('body-parser'); 4 | var fs = require('fs'); 5 | 6 | // import model schemas 7 | const User = require('../../js/db/models/User'); 8 | const { sitemapPath, sitemapFunction } = require('../../js/sitemap') 9 | 10 | // import an instance from express Router 11 | const app = express.Router(); 12 | 13 | // Bodyparser middlewares 14 | app.use(bodyParser.json()); 15 | app.use(bodyParser.urlencoded({ 16 | extended: true 17 | })); 18 | 19 | /** 20 | * @api {post} /api/restore Restore User 21 | * @apiGroup Restore 22 | * @apiName RestoreUser 23 | * 24 | * @apiVersion 1.0.0 25 | * 26 | * @apiDescription Restore the user 27 | * 28 | * @apiUse AuthorizationHeader 29 | * 30 | * @apiParam {Object} body 31 | * @apiParam {String} body.user User who has been deleted 32 | * 33 | * @apiError (500) InternalServerError An error occurred finding, updating, or saving the user 34 | * @apiUse AuthorizationError 35 | */ 36 | app.post('/', (req, res) => { 37 | const user = req.body.user; 38 | User.findOne({_id: user._id}, (err, user) => { 39 | if(err) { 40 | res.sendStatus(500); 41 | } else if(user) { 42 | // change user.deleted to false to restore 43 | user.deleted = false; 44 | user.save((err, user) => { 45 | if(err) { 46 | res.sendStatus(500); 47 | } else { 48 | // updating the sitemap to reflect the restoration of user 49 | sitemapFunction().then((resp) => { 50 | fs.writeFileSync(sitemapPath, resp.toString()); 51 | }) 52 | res.send({message: 'Success'}); 53 | } 54 | }) 55 | } 56 | }) 57 | }); 58 | 59 | // export the app to be used elsewhere 60 | module.exports = app; 61 | -------------------------------------------------------------------------------- /client/src/reducers/Utils.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOG_REQUEST_SUBMISSION_START, 3 | LOG_REQUEST_SUBMISSION_END 4 | } from '../actions/utils' 5 | 6 | export const LogReducer = (logs = {}, action) => { 7 | switch (action.type) { 8 | case LOG_REQUEST_SUBMISSION_START: 9 | return Object.assign({}, logs, { 10 | [action.submissionID]: RequestReducer(logs[action.submissionID], action) 11 | }) 12 | case LOG_REQUEST_SUBMISSION_END: 13 | return Object.assign({}, logs, { 14 | [action.submissionID]: RequestReducer(logs[action.submissionID], action) 15 | 16 | }) 17 | default: 18 | return logs 19 | } 20 | } 21 | 22 | const RequestReducer = (request = {}, action) => { 23 | switch (action.type) { 24 | case LOG_REQUEST_SUBMISSION_START: 25 | return Object.assign({}, request, { 26 | [action.id]: TimeReducer(request[action.id], action) 27 | }) 28 | case LOG_REQUEST_SUBMISSION_END: 29 | return Object.assign({}, request, { 30 | [action.id]: TimeReducer(request[action.id], action) 31 | }) 32 | default: 33 | return request 34 | } 35 | } 36 | 37 | const TimeReducer = (timeLogs = {}, action) => { 38 | switch (action.type) { 39 | case LOG_REQUEST_SUBMISSION_START: 40 | return Object.assign({}, timeLogs, { 41 | start: action.time 42 | }) 43 | case LOG_REQUEST_SUBMISSION_END: 44 | var start = new Date(timeLogs.start); 45 | var end = new Date(action.time); 46 | var timeDifference = (end.getTime() - start.getTime()) / 1000 47 | return Object.assign({}, timeLogs, { 48 | end: action.time, 49 | size: action.size, 50 | timeDifference 51 | }) 52 | default: 53 | return timeLogs 54 | } 55 | } -------------------------------------------------------------------------------- /server/js/db/migrations/1545351944251-add-authorName-Submissions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // modules ================================== 3 | var mongoose = require('mongoose'); 4 | var config = require('../_config'); 5 | var Submission = require('../models/Submission'); 6 | 7 | // config =================================== 8 | const options = { 9 | autoReconnect: true, 10 | reconnectTries: 5, 11 | reconnectInterval: 2000 12 | } 13 | 14 | /** 15 | * this function performs the migration operation to add authorName field in submissions collection, 16 | * by getting its value from users collection 17 | */ 18 | 19 | module.exports.up = function (next) { 20 | return mongoose.connect(config.url, options).then(db => { 21 | return Submission.aggregate([ 22 | { 23 | $lookup: 24 | { 25 | from: "users", 26 | localField: "author", 27 | foreignField: "_id", 28 | as: "authorInfo" 29 | } 30 | }, 31 | { 32 | $addFields: 33 | { 34 | authorName: { $reduce: { input: "$authorInfo.name", initialValue: "", in: { $concat : ["$$value", "$$this"] }} } 35 | } 36 | }, 37 | { 38 | $project: 39 | { 40 | "authorInfo": 0 41 | } 42 | }, 43 | { 44 | $out: "submissions" 45 | } 46 | ]) 47 | }) 48 | .then(() => { 49 | mongoose.close() 50 | return next() 51 | }) 52 | .catch(err => next(err)) 53 | } 54 | 55 | /** 56 | * This function removes the authorName field from the submission collection, 57 | * which was added by the up migration function above 58 | */ 59 | 60 | module.exports.down = function (next) { 61 | return mongoose.connect(config.url, options).then(db => { 62 | return Submission.update({ 63 | authorName: { $exists: true } 64 | }, 65 | { 66 | $unset: { authorName: "" }, 67 | }, 68 | { multi: true } 69 | ) 70 | }) 71 | .then(() => { 72 | mongoose.close() 73 | return next() 74 | }) 75 | .catch(err => next(err)) 76 | } 77 | -------------------------------------------------------------------------------- /server/js/db/models/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tlyon on 5/26/17. 3 | */ 4 | 5 | // import modules 6 | var mongoose = require('mongoose'); 7 | var mongoosePaginate = require('mongoose-paginate'); 8 | 9 | // create the schema object from mongoose 10 | var Schema = mongoose.Schema; 11 | var ObjectId = Schema.ObjectId; 12 | 13 | // user schema 14 | var userSchema = new Schema({ 15 | // info 16 | name: String, 17 | views: Number, 18 | numComments: Number, 19 | joinDate: Date, 20 | voteScore: Number, 21 | summary: String, 22 | submissions: [ObjectId], 23 | deletedSubmissions: [ObjectId], 24 | currentSubmission: Object, 25 | upvotes: [ObjectId], 26 | downvotes: [ObjectId], 27 | avatar: String, 28 | // social media/websites 29 | website: String, 30 | email: String, 31 | summary: String, 32 | activeAvatar: String, 33 | emailSettings: { 34 | newComment: Boolean, 35 | newReply: Boolean, 36 | submission: Boolean 37 | }, 38 | github: { 39 | id: String, 40 | access_token: String, 41 | url: String, 42 | username: String, 43 | hidden: Boolean, 44 | avatarURL: String, 45 | }, 46 | fb: { 47 | id: String, 48 | access_token: String, 49 | displayName: String, 50 | url: String, 51 | hidden: Boolean, 52 | avatarURL: String, 53 | }, 54 | google: { 55 | id: String, 56 | avatarURL: String, 57 | access_token: String, 58 | hidden: Boolean, 59 | displayName: String 60 | }, 61 | twitter: { 62 | id: String, 63 | access_token: String, 64 | username: String, 65 | avatarURL: String, 66 | url: String, 67 | hidden: Boolean 68 | }, 69 | oneSocial: Boolean, 70 | currentToken: String, 71 | currentProvider: String, 72 | // meta 73 | flagged: Boolean, 74 | flaggedReason: String, 75 | // archive the user 76 | deleted: Boolean, 77 | new: Boolean, 78 | }); 79 | 80 | userSchema.plugin(mongoosePaginate); 81 | 82 | module.exports = mongoose.model("User", userSchema); 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notes", 3 | "version": "1.1.0", 4 | "description": "Code for QuantEcon's Notes site", 5 | "private": true, 6 | "main": "./server/app.js", 7 | "scripts": { 8 | "start": "cd ./server && nodemon app.js", 9 | "start-client": "cd ./client && npm start", 10 | "dev": "concurrently \"npm run start\" \"npm run start-client\" \"cd ./client/src/assets/scss && gulp\"", 11 | "install-server": "cd ./server && npm install", 12 | "install-client": "cd ./client && npm install", 13 | "build-client": "cd ./client && npm run build", 14 | "api-docs": "apidoc -f \".*\\.js$\" -i ./server/routes -o ./docs", 15 | "client-docs": "jsdoc -r -d ./client/docs -c ./jsdoc-config.json", 16 | "setup-css": "cd client/src/assets && npm install --global gulp-cli && npm install && gulp", 17 | "clean": "concurrently \"npm run cliean-client\" \"npm run clean-server\"", 18 | "clean-client": "cd ./client && rm -rf node_modules && rm -rf build", 19 | "clean-server": "cd ./server && rm -rf node_modules", 20 | "config": "cd scripts && python dev-config.py", 21 | "build-docker-server": "docker build -t bookshelf-server:0.1 -f ./Dockerfile-server .", 22 | "docker": "docker-compose up", 23 | "stop-docker": "docker-compose stop", 24 | "docker-daemon": "docker-compose up -d" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/jstac/QuantEconLib.git" 29 | }, 30 | "author": "@tlyon3", 31 | "license": "BSD-3", 32 | "bugs": { 33 | "url": "https://github.com/jstac/QuantEconLib/issues" 34 | }, 35 | "homepage": "https://github.com/jstac/QuantEconLib#readme", 36 | "apidoc": { 37 | "title": "Bookshelf-Documentation", 38 | "description": "API documentation for Bookshelf site", 39 | "url": "http://bookshelf.quantecon.org", 40 | "order": [ 41 | "Information", 42 | "Search", 43 | "Submit", 44 | "Edit", 45 | "Delete", 46 | "Vote", 47 | "Edit_Profile", 48 | "Authentication" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "concurrently": "^3.5.1", 53 | "jsdoc-jsx": "^0.1.0", 54 | "nodemon": "^1.18.9" 55 | }, 56 | "dependencies": { 57 | "email-validator": "^2.0.4", 58 | "react-tabs": "^2.3.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/src/reducers/Announcements.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_ANNOUNCEMENTS, 3 | RECEIVE_ANNOUCEMENTS, 4 | ADD_ANNOUNCEMENT, 5 | CHANGE_ANNOUNCEMENT, 6 | RECEIVE_RECENT, 7 | DELETE_RECENT 8 | } from '../actions/announcements' 9 | 10 | const AnnoucnementsReducer = (announcements = {}, action) => { 11 | switch(action.type){ 12 | case REQUEST_ANNOUNCEMENTS: 13 | return Object.assign({}, announcements, { 14 | isFetching: true 15 | }) 16 | case RECEIVE_ANNOUCEMENTS: 17 | return Object.assign({}, announcements, { 18 | isFetching: false, 19 | announcements: action.announcements, 20 | error: action.error 21 | }) 22 | case ADD_ANNOUNCEMENT: 23 | if(action.error){ 24 | console.warn('[AnnouncementsReducers] - api returned error: ', action.error) 25 | } 26 | 27 | return Object.assign({}, announcements, { 28 | // For now, just replace the array 29 | announcements: [action.announcement], 30 | error: action.error 31 | }) 32 | case CHANGE_ANNOUNCEMENT: 33 | if(action.error){ 34 | console.warn('[AnnouncementsReducers] - api returned error: ', action.error) 35 | } 36 | 37 | return Object.assign({}, announcements, { 38 | recent: action.announcement, 39 | error: action.error 40 | }) 41 | case RECEIVE_RECENT: 42 | if(action.error){ 43 | console.warn('[AnnouncementsReducers] - api returned error: ', action.error) 44 | } 45 | 46 | return Object.assign({}, announcements, { 47 | isFetching: false, 48 | recent: action.announcement, 49 | error: action.error 50 | }) 51 | case DELETE_RECENT: 52 | if(action.error){ 53 | console.warn('[AnnouncementsReducers] - api returned error: ', action.error) 54 | } 55 | 56 | return Object.assign({}, announcements, { 57 | recent: null, 58 | error: action.error 59 | }) 60 | default: 61 | return announcements 62 | 63 | } 64 | } 65 | 66 | 67 | 68 | export default AnnoucnementsReducer -------------------------------------------------------------------------------- /client/src/containers/HeadContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux' 4 | import Head from '../components/partials/Head'; 5 | import {resetSearchParams} from '../actions/submissionList'; 6 | import CheckmarkIcon from 'react-icons/lib/fa/check-circle-o'; 7 | 8 | class HeadContainer extends Component { 9 | 10 | constructor(props){ 11 | super(props) 12 | this.state = { 13 | sentSuccess: null 14 | }; 15 | 16 | this.handleSentSuccess = this.handleSentSuccess.bind(this) 17 | this.resetParams = this.resetParams.bind(this) 18 | } 19 | 20 | handleSentSuccess = (truthValue) => { 21 | this.setState({sentSuccess: truthValue}); 22 | } 23 | 24 | resetParams = () => { 25 | console.log("[HeadContainer] - reset search params") 26 | this.props.actions.resetSearchParams() 27 | } 28 | 29 | render() { 30 | return ( 31 |
    32 | 39 | { this.state.sentSuccess && this.state.sentSuccess.sentValue ? 40 |
    41 |
    42 |

    43 | 44 | Email was successfully sent to {this.state.sentSuccess.sentEmail} 45 |

    46 |
    47 |
    48 | : null } 49 |
    50 | ) 51 | } 52 | } 53 | 54 | function mapStateToProps(state, props) { 55 | return {isSignedIn: state.auth.isSignedIn, user: state.auth.user, isAdmin: state.auth.isAdmin} 56 | } 57 | 58 | function mapDispatchToProps(dispatch) { 59 | return { 60 | actions: bindActionCreators({ 61 | resetSearchParams 62 | }, dispatch) 63 | } 64 | } 65 | 66 | export default connect(mapStateToProps, mapDispatchToProps)(HeadContainer); 67 | -------------------------------------------------------------------------------- /server/js/sitemap.js: -------------------------------------------------------------------------------- 1 | var sm = require('sitemap') 2 | const path = require('path'); 3 | var { url } = require('../_config'); 4 | var Submission = require('./db/models/Submission'); 5 | var User = require('./db/models/User'); 6 | // sitemap 7 | const sitemapPath = path.join(__dirname, '/../../client/build/sitemap.xml') 8 | 9 | let sitemapFunction = async function siteMap() { 10 | let submissionsUrls = []; 11 | let userUrls = []; 12 | const submissionPromise = Submission.find({},(err, submissionsList) => { 13 | if (err) { 14 | console.log("error getting submission urls for sitemap") 15 | } else { 16 | submissionsList.map(function(submission) { 17 | if (!submission.deleted) { 18 | submissionsUrls.push({ 19 | url: '/submission/'+submission._id, 20 | changefreq: 'weekly', 21 | priority: 0.9 22 | }) 23 | submissionsUrls.push({ 24 | url: '/submission/'+submission._id + '/comments', 25 | changefreq: 'weekly', 26 | priority: 0.8 27 | }) 28 | } 29 | }) 30 | } 31 | }) 32 | const userPromise = User.find({},(err, usersList) => { 33 | if (err) { 34 | console.log("error getting user urls for sitemap") 35 | } else { 36 | usersList.map(function(user) { 37 | if (!user.deleted) { 38 | userUrls.push({ 39 | url: '/user/'+user._id, 40 | changefreq: 'weekly', 41 | priority: 0.8 42 | }) 43 | } 44 | }) 45 | } 46 | }) 47 | await submissionPromise; 48 | await userPromise; 49 | sitemap = sm.createSitemap({ 50 | hostname: url, 51 | cacheTime: 600000, // 600 sec - cache purge period 52 | urls: [ 53 | { url: '', priority: 0.9, changefreq: 'daily' }, 54 | { url: '/about/', priority: 0.6, changefreq: 'monthly'}, 55 | { url: '/contact/', priority: 0.6, changefreq: 'monthly'}, 56 | ...submissionsUrls, 57 | ...userUrls 58 | ] 59 | }); 60 | return sitemap; 61 | } 62 | 63 | module.exports = { 64 | sitemapFunction, 65 | sitemapPath 66 | } 67 | -------------------------------------------------------------------------------- /server/js/db/migrations/1555330744208-resize-avatars-to-50px-twitter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // modules ================================== 4 | let mongoose = require('mongoose'); 5 | let config = require('./../_config'); 6 | let Users = require('../models/User'); 7 | 8 | // config =================================== 9 | const options = { 10 | autoReconnect: true, 11 | reconnectTries: 5, 12 | reconnectInterval: 2000 13 | } 14 | 15 | module.exports.up = function (next) { 16 | return mongoose.connect(config.url, options).then(db => { 17 | return Users.find({ $and: [{ currentProvider: "Twitter" }, { avatar: { $regex: /^((?!_normal).)*$/ } }] }, function(err, docs) { 18 | for (let e in docs) { 19 | let doc = docs[e]; 20 | if (doc.avatar.indexOf('.jpeg') > -1) { 21 | doc.avatar = doc.avatar.replace('.jpeg', '_normal.jpeg'); 22 | } else if (doc.avatar.indexOf('.png') > -1) { 23 | doc.avatar = doc.avatar.replace('.png', '_normal.png'); 24 | } else if (doc.avatar.indexOf('.jpg') > -1) { 25 | doc.avatar = doc.avatar.replace('.jpg', '_normal.jpg'); 26 | }; 27 | doc.save((err) => { 28 | if (err) { 29 | console.log(err); 30 | } else { 31 | } 32 | }) 33 | } 34 | }) 35 | }) 36 | .then(() => { 37 | mongoose.close() 38 | return next() 39 | }) 40 | .catch(err => next(err)) 41 | } 42 | 43 | module.exports.down = function (next) { 44 | return mongoose.connect(config.url, options).then(db => { 45 | return Users.find({ $and: [{ currentProvider: "Twitter" }, { avatar: { $regex: /^((?_normal).)*$/ } }] }, function(err, docs) { 46 | for (let e in docs) { 47 | let doc = docs[e]; 48 | if (doc.avatar.indexOf('.jpeg') > -1) { 49 | doc.avatar = doc.avatar.replace('_normal.jpeg', '.jpeg'); 50 | } else if (doc.avatar.indexOf('.png') > -1) { 51 | doc.avatar = doc.avatar.replace('_normal.png', '.png'); 52 | } else if (doc.avatar.indexOf('.jpg') > -1) { 53 | doc.avatar = doc.avatar.replace('_normal.jpg', '.jpg'); 54 | }; 55 | doc.save((err) => { 56 | if (err) { 57 | console.log(err); 58 | } else { 59 | } 60 | }) 61 | } 62 | }) 63 | }) 64 | .then(() => { 65 | mongoose.close() 66 | return next() 67 | }) 68 | .catch(err => next(err)) 69 | } -------------------------------------------------------------------------------- /client/src/utils/trimText.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: Alexanders Manning 3 | * Github source code: https://github.com/alexandersmanning/read-more-react#readme 4 | * 5 | * trimText returns truncated text at appropriate point given a minimum, ideal, and maximum text length. 6 | * @param {String} text 7 | * @param {Number} minimum 8 | * @param {Number} ideal 9 | * @param {Number} maximum 10 | * 11 | */ 12 | const PUNCTUATION_LIST = [".",",","!","?","'","{","}","(",")","[","]", "/"]; 13 | 14 | const trimText = (text, min = 80, ideal = 100, max = 200) => { 15 | //This main function uses two pointers to move out from the ideal, to find the first instance of a punctuation mark followed by a space. If one cannot be found, it will go with the first space closest to the ideal. 16 | 17 | if (max < min || ideal > max || ideal < min) { 18 | throw new Error("The minimum length must be less than the maximum, and the ideal must be between the minimum and maximum.") 19 | } 20 | 21 | if (text.length < ideal) { 22 | return [text, '']; 23 | } 24 | 25 | let pointerOne = ideal; 26 | let pointerTwo = ideal; 27 | let firstSpace, resultIdx; 28 | 29 | const setSpace = (idx) => { 30 | if (spaceMatch(text[idx])) { firstSpace = firstSpace || idx } 31 | } 32 | 33 | while (pointerOne < max || pointerTwo > min) { 34 | if (checkMatch(pointerOne, text, max, min)) { 35 | resultIdx = pointerOne + 1 36 | break; 37 | } else if (checkMatch(pointerTwo, text, max, min)) { 38 | resultIdx = pointerTwo + 1; 39 | break; 40 | } else { 41 | setSpace(pointerOne); 42 | setSpace(pointerTwo); 43 | } 44 | 45 | pointerOne++; 46 | pointerTwo--; 47 | } 48 | 49 | if (resultIdx === undefined) { 50 | if (firstSpace && firstSpace >= min && firstSpace <= max) { 51 | resultIdx = firstSpace; 52 | } else if (ideal - min < max - ideal) { 53 | resultIdx = min; 54 | } else { 55 | resultIdx = max; 56 | } 57 | } 58 | 59 | return [text.slice(0, resultIdx), text.slice(resultIdx).trim()]; 60 | } 61 | 62 | const spaceMatch = (character) => { 63 | if (character === " ") { return true } 64 | } 65 | 66 | const punctuationMatch = (idx, text) => { 67 | let punctuationIdx = PUNCTUATION_LIST.indexOf(text[idx]); 68 | if (punctuationIdx >= 0 && spaceMatch(text[idx + 1])) 69 | { 70 | return true; 71 | } 72 | } 73 | 74 | const checkMatch = (idx, text, max, min) => { 75 | if (idx < max && idx > min && punctuationMatch(idx, text)) { 76 | return true; 77 | } 78 | } 79 | 80 | export default trimText; -------------------------------------------------------------------------------- /client/src/reducers/SubmissionList.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_SUBMISSION_PREVIEWS, 3 | RECEIVE_SUBMISSION_PREVIEWS, 4 | INVALIDATE_SUBMISSION_LIST, 5 | RESET_SEARCH_PARAMS 6 | } from '../actions/submissionList'; 7 | 8 | const defaultSearchParams = { 9 | lang: 'All', 10 | time: 'All time', 11 | topic: 'All', 12 | author: '', 13 | keywords: '', 14 | page: 1, 15 | sortBy: 'Discover' 16 | } 17 | 18 | const SubmissionList = (state = { 19 | isFetching: false, 20 | didInvalidate: false, 21 | submissionByID: {}, 22 | submissionList: {} 23 | }, action) => { 24 | switch (action.type) { 25 | case INVALIDATE_SUBMISSION_LIST: 26 | return Object.assign({}, state, { 27 | didInvalidate: true 28 | }); 29 | 30 | case REQUEST_SUBMISSION_PREVIEWS: 31 | // console.log('[Middleware] - State: ', state); 32 | // console.log('[Middleware] - Action: ', action); 33 | return Object.assign({}, state, { 34 | isFetching: true, 35 | didInvalidate: false 36 | }); 37 | 38 | case RESET_SEARCH_PARAMS: 39 | return Object.assign({}, state, { 40 | searchParams: defaultSearchParams 41 | }) 42 | case RECEIVE_SUBMISSION_PREVIEWS: 43 | // console.log('[Middleware ] - State: ', state); 44 | return Object.assign({}, state, { 45 | isFetching: false, 46 | didInvalidate: false, 47 | previews: action.submissionList, 48 | searchParams: action.searchParams, 49 | totalSubmissions: action.totalSubmissions, 50 | authors: action.authors, 51 | languages: action.languages, 52 | lastUpdated: action.receivedAt 53 | }); 54 | 55 | default: 56 | return state; 57 | } 58 | } 59 | 60 | const SubmissionListReducer = (state = {}, action) => { 61 | switch (action.type) { 62 | case REQUEST_SUBMISSION_PREVIEWS: 63 | return SubmissionList(state, action); 64 | case RECEIVE_SUBMISSION_PREVIEWS: 65 | return SubmissionList(state, action); 66 | case RESET_SEARCH_PARAMS: 67 | return SubmissionList(state, action) 68 | case INVALIDATE_SUBMISSION_LIST: 69 | break; 70 | default: 71 | return state; 72 | } 73 | } 74 | 75 | export default SubmissionListReducer; 76 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.3.0", 4 | "private": true, 5 | "dependencies": { 6 | "@nteract/markdown": "^1.0.0", 7 | "@nteract/notebook-preview": "^7.1.2", 8 | "axios": "^0.16.2", 9 | "file-saver": "^1.3.8", 10 | "history": "^4.6.3", 11 | "latest": "^0.2.0", 12 | "mathjax": "^2.7.1", 13 | "mathjax-electron": "^2.0.1", 14 | "moment": "^2.18.1", 15 | "node-sass-chokidar": "^1.3.4", 16 | "normalize-css": "^2.3.1", 17 | "npm-run-all": "^4.1.5", 18 | "object-sizeof": "^1.2.0", 19 | "react": "^16.2.0", 20 | "react-bootstrap": "^0.31.0", 21 | "react-confirm-alert": "^1.0.8", 22 | "react-dom": "^16.2.0", 23 | "react-dropzone": "^4.2.0", 24 | "react-html-parser": "^2.0.1", 25 | "react-icons": "^2.2.5", 26 | "react-lazyload": "^2.5.0", 27 | "react-markdown": "^3.1.4", 28 | "react-mathjax": "git+https://github.com/wko27/react-mathjax.git", 29 | "react-meta-tags": "^0.7.3", 30 | "react-modal": "^3.0.0", 31 | "react-moment": "^0.7.0", 32 | "react-paginate": "^5.0.0", 33 | "react-redux": "^5.0.5", 34 | "react-render-html": "^0.5.2", 35 | "react-router": "^4.1.1", 36 | "react-router-dom": "^4.1.2", 37 | "react-router-redux": "^4.0.8", 38 | "react-scripts": "^2.1.5", 39 | "react-tabs": "^2.3.0", 40 | "react-time": "^4.3.0", 41 | "redux": "^3.7.2", 42 | "redux-observable": "^0.17.0", 43 | "redux-thunk": "^2.2.0", 44 | "remark-math": "^1.0.3", 45 | "source-map-explorer": "^1.4.0", 46 | "styled-jsx": "^2.2.1", 47 | "typeface-source-code-pro": "0.0.35", 48 | "typeface-source-sans-pro": "0.0.35" 49 | }, 50 | "scripts": { 51 | "analyze": "source-map-explorer build/static/js/main.*", 52 | "start-js": "react-scripts start", 53 | "start": "npm-run-all -p watch-css start-js", 54 | "build": "npm run build-css && react-scripts build", 55 | "test": "react-scripts test --env=jsdom", 56 | "eject": "react-scripts eject", 57 | "build-css": "node-sass-chokidar src/assets/scss/src/main.scss -o src/assets/css --output-style compressed", 58 | "watch-css": "npm run build-css && node-sass-chokidar src/assets/scss/src/main.scss -o src/assets/css --watch --recursive" 59 | }, 60 | "proxy": "http://localhost:8080", 61 | "devDependencies": { 62 | "docdash": "^0.4.0", 63 | "jsdoc-jsx": "^0.1.0", 64 | "minami": "^1.2.3", 65 | "nodemon": "^1.18.9", 66 | "query-string": "^6.2.0" 67 | }, 68 | "browserslist": [ 69 | ">0.2%", 70 | "not dead", 71 | "not ie <= 11", 72 | "not op_mini all" 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /client/src/actions/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file User actions 3 | * @author Trevor Lyon 4 | * 5 | * @module userActions 6 | */ 7 | 8 | import store from '../store/store'; 9 | import axios from 'axios' 10 | 11 | export const REQUEST_USER_INFO = 'REQUEST_USER_INFO' 12 | export const requestUserInfo = (userID = null) => { 13 | return { 14 | type: REQUEST_USER_INFO, 15 | userID 16 | } 17 | } 18 | 19 | export const RECEIVE_USER_INFO = 'RECEIVE_USER_INFO' 20 | export const receiveUserInfo = (userID, json) => { 21 | return { 22 | type: RECEIVE_USER_INFO, 23 | userID, 24 | data: json[0], 25 | receivedAt: Date.now() 26 | } 27 | } 28 | 29 | export const REQUIRE_SIGN_IN = 'REQUIRE_SIGN_IN' 30 | export const requireSignIn = () => { 31 | return { 32 | type: REQUIRE_SIGN_IN, 33 | userID: 'my-profile' 34 | } 35 | } 36 | 37 | export const INVALIDATE_USER_INFO = 'INVALIDATE_USER_INFO' 38 | export const invalidateUserInfo = (userID) => { 39 | return { 40 | type: INVALIDATE_USER_INFO, 41 | userID 42 | } 43 | } 44 | 45 | export const FLAG_USER = "FLAG_USER" 46 | const flagUserAction = ({userID, error}) => { 47 | return { 48 | type: FLAG_USER, 49 | error, 50 | userID 51 | } 52 | } 53 | 54 | // ================================================== 55 | 56 | export const flagUser = ({userID}) => { 57 | return (dispatch) => { 58 | axios.post("/api/flag/user", {userID}).then( 59 | resp => { 60 | dispatch(flagUserAction({userID})) 61 | }, 62 | err => { 63 | dispatch(flagUserAction({error: err})) 64 | } 65 | ) 66 | } 67 | } 68 | 69 | 70 | /** 71 | * @function fetchUserInfo 72 | * @description Makes an API request to fetch all the data for the user with the matching ID 73 | * @param {String} userID ID of the user being searched for 74 | */ 75 | export const fetchUserInfo = (userID) => { 76 | return function(dispatch) { 77 | dispatch(requestUserInfo(userID)); 78 | const state = store.getState(); 79 | if(userID === 'my-profile'){ 80 | if(!state.auth.isSignedIn){ 81 | dispatch(requireSignIn()); 82 | return; 83 | } 84 | userID = state.auth.user._id; 85 | } 86 | fetch('/api/search/users/?_id=' + userID).then( 87 | results => {return results.json();}, 88 | error => {console.log('An error ocurred: ', error)} 89 | ).then(data => { 90 | dispatch(receiveUserInfo(userID, data)) 91 | }); 92 | } 93 | } -------------------------------------------------------------------------------- /client/src/containers/submission/EditSubmissionPreviewContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux' 4 | import Preview from '../../components/submit/Preview'; 5 | import * as editSubmissionActions from '../../actions/editSubmission' 6 | import {editSubmission} from '../../actions/submission' 7 | 8 | class EditSubmissionPreviewContainer extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.cancelPreview = this 12 | .cancelPreview 13 | .bind(this); 14 | this.save = this 15 | .save 16 | .bind(this); 17 | } 18 | 19 | cancelPreview = () => { 20 | console.log('[EditSubmissionPreviewContainer] - submission id: ', this.props.match.params.id); 21 | this 22 | .props 23 | .editSubmissionActions 24 | .cancelPreview({submissionID: this.props.match.params.id}); 25 | this 26 | .props 27 | .history 28 | .replace('/submission/' + this.props.match.params.id); 29 | } 30 | 31 | save = () => { 32 | console.log('[EditSubmissionPreviewContainer] - save clicked'); 33 | this 34 | .props 35 | .editSubmissionActions 36 | .saveSubmission({submissionID: this.props.match.params.id}) 37 | this 38 | .props 39 | .submissionActions 40 | .editSubmission({submissionData: this.props.submission}); 41 | this.props.history.replace('/submission/' + this.props.match.params.id); 42 | } 43 | 44 | render() { 45 | return ( 46 |
    47 | 54 |
    55 | ) 56 | } 57 | } 58 | 59 | const mapStateToProps = (state, props) => { 60 | 61 | return { 62 | currentUser: state.auth.user, 63 | submission: state.editSubmissionByID[props.match.params.id] 64 | } 65 | } 66 | 67 | const mapDispatchToProps = (dispatch) => { 68 | return { 69 | editSubmissionActions: bindActionCreators(editSubmissionActions, dispatch), 70 | submissionActions: bindActionCreators({ 71 | editSubmission 72 | }, dispatch) 73 | } 74 | } 75 | 76 | export default connect(mapStateToProps, mapDispatchToProps)(EditSubmissionPreviewContainer); -------------------------------------------------------------------------------- /client/src/remark-math/inline.js: -------------------------------------------------------------------------------- 1 | function locator (value, fromIndex) { 2 | return value.indexOf('$', fromIndex) 3 | } 4 | 5 | const ESCAPED_INLINE_MATH = /^\\\$/ 6 | const INLINE_MATH = /^\$((?:\\\$|[^$])+)\$/ 7 | const INLINE_MATH_DOUBLE = /^\$\$((?:\\\$|[^$])+)\$\$/ 8 | 9 | module.exports = function inlinePlugin (opts) { 10 | function inlineTokenizer (eat, value, silent) { 11 | let isDouble = true 12 | let match = INLINE_MATH_DOUBLE.exec(value) 13 | if (!match) { 14 | match = INLINE_MATH.exec(value) 15 | isDouble = false 16 | } 17 | const escaped = ESCAPED_INLINE_MATH.exec(value) 18 | 19 | if (escaped) { 20 | /* istanbul ignore if - never used (yet) */ 21 | if (silent) { 22 | return true 23 | } 24 | return eat(escaped[0])({ 25 | type: 'text', 26 | value: '$' 27 | }) 28 | } 29 | 30 | if (value.slice(-2) === '\\$') { 31 | return eat(value)({ 32 | type: 'text', 33 | value: value.slice(0, -2) + '$' 34 | }) 35 | } 36 | 37 | if (match) { 38 | /* istanbul ignore if - never used (yet) */ 39 | if (silent) { 40 | return true 41 | } 42 | 43 | const endingDollarInBackticks = match[0].includes('`') && value.slice(match[0].length).includes('`') 44 | if (endingDollarInBackticks) { 45 | const toEat = value.slice(0, value.indexOf('`')) 46 | return eat(toEat)({ 47 | type: 'text', 48 | value: toEat 49 | }) 50 | } 51 | 52 | const trimmedContent = match[1].trim() 53 | 54 | return eat(match[0])({ 55 | type: 'inlineMath', 56 | value: trimmedContent, 57 | data: { 58 | hName: 'span', 59 | hProperties: { 60 | className: 'inlineMath' + (isDouble && opts.inlineMathDouble ? ' inlineMathDouble' : '') 61 | }, 62 | hChildren: [ 63 | { 64 | type: 'text', 65 | value: trimmedContent 66 | } 67 | ] 68 | } 69 | }) 70 | } 71 | } 72 | inlineTokenizer.locator = locator 73 | 74 | const Parser = this.Parser 75 | 76 | // Inject inlineTokenizer 77 | const inlineTokenizers = Parser.prototype.inlineTokenizers 78 | const inlineMethods = Parser.prototype.inlineMethods 79 | inlineTokenizers.math = inlineTokenizer 80 | inlineMethods.splice(inlineMethods.indexOf('text'), 0, 'math') 81 | 82 | const Compiler = this.Compiler 83 | 84 | // Stringify for math inline 85 | if (Compiler != null) { 86 | const visitors = Compiler.prototype.visitors 87 | visitors.inlineMath = function (node) { 88 | return '$' + node.value + '$' 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /client/src/reducers/EditSubmission.js: -------------------------------------------------------------------------------- 1 | import { 2 | // BUILD_SUBMISSION_PREVIEW, 3 | // PREVIEW, 4 | // CANCEL_PREVIEW, 5 | SAVE_SUBMISSION 6 | } from '../actions/editSubmission' 7 | 8 | const SubmissionReducer = (submission = {}, action) => { 9 | switch (action.type) { 10 | // case BUILD_SUBMISSION_PREVIEW: 11 | // return Object.assign({}, submission, { 12 | // ...action.submission, 13 | // isLoading: false 14 | // }) 15 | // case PREVIEW: 16 | // return Object.assign({}, submission, {isLoading: true}) 17 | case SAVE_SUBMISSION: 18 | console.log("[SubmissionReducer] (SAVE_SUBMISSION) - action: ", action) 19 | return null 20 | default: 21 | // console.warn('[SubmissionReducer] - returning default in reducer'); 22 | return submission 23 | } 24 | 25 | } 26 | 27 | const EditSubmissionRecucer = (editSubmissionByID = {}, action) => { 28 | if (action.error) { 29 | return Object.assign({}, editSubmissionByID, { 30 | error: action.error 31 | }) 32 | } 33 | switch (action.type) { 34 | // case BUILD_SUBMISSION_PREVIEW: 35 | // if (action.error) { 36 | // return Object.assign({}, editSubmissionByID, {error: action.error}) 37 | // } 38 | // return Object.assign({}, editSubmissionByID, { 39 | // [action.submission._id]: SubmissionReducer(editSubmissionByID[action.submission._id], action) 40 | // }) 41 | 42 | // case CANCEL_PREVIEW: 43 | // if (action.error) { 44 | // return Object.assign({}, editSubmissionByID, {error: action.error}) 45 | // } 46 | // return Object.assign({}, editSubmissionByID, { 47 | // [action.submissionID]: null 48 | // }) 49 | case SAVE_SUBMISSION: 50 | if (action.error) { 51 | return Object.assign({}, editSubmissionByID, { 52 | error: action.error 53 | }) 54 | } 55 | return Object.assign({}, editSubmissionByID, { 56 | [action.submissionID]: SubmissionReducer(editSubmissionByID[action.submissionID], action) 57 | }) 58 | // case PREVIEW: 59 | // return Object.assign({}, editSubmissionByID, { 60 | // [action.submissionID]: SubmissionReducer(editSubmissionByID[action.submissionID], action) 61 | // }) 62 | 63 | default: 64 | // console.warn('[EditSubmissionReducer] - returning default in reducer'); 65 | return editSubmissionByID 66 | } 67 | } 68 | 69 | export default EditSubmissionRecucer -------------------------------------------------------------------------------- /server/js/auth/adminjwt.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const JwtStrategy = require('passport-jwt').Strategy; 3 | const ExtractJwt = require('passport-jwt').ExtractJwt; 4 | const _config = require('../../_config'); 5 | const User = require('../db/models/User') 6 | const AdminList = require('../db/models/AdminList') 7 | 8 | var opts = { 9 | jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderWithScheme('jwt'), ExtractJwt.fromUrlQueryParameter('jwt')]), 10 | secretOrKey: "banana horse laser muffin" 11 | } 12 | 13 | const select = 'name views numComments joinDate voteScore position submissions upvotes downvotes' + 14 | ' avatar website email summary activeAvatar currentProvider github fb twitter google oneSocial emailSettings new' 15 | 16 | passport.use('adminjwt', new JwtStrategy(opts, function (jwt_payload, done) { 17 | if(jwt_payload.isAdmin){ 18 | // Check userid is listed as an admin 19 | AdminList.findOne({}, (err, adminList) => { 20 | if(err){ 21 | console.warn("401: UNAUTHORIZED - Error getting admin list from database") 22 | return done({message: "Error getting admin list from database", code: "5-10"}, null) 23 | } else if(adminList){ 24 | console.log("[AdminJWT] - adminList: ", adminList) 25 | 26 | if(adminList.adminIDs && adminList.adminIDs.indexOf(jwt_payload.user._id) != -1){ 27 | User.findById(jwt_payload.user._id, (err, user) => { 28 | if (err){ 29 | console.warn("401: UNAUTHORIZED - Error finding user in database: " , err) 30 | return done({message: "Error finding user in database", code:"5-11"}) 31 | } else if (user){ 32 | return done(null, user) 33 | } else { 34 | console.warn("401: UNAUTHORIZED - Couldn't find user in database") 35 | return done({message: "Couldn't find user in database", code:"5-20"}, null) 36 | } 37 | }) 38 | } else { 39 | // TODO: SOMETHING BAD IS HAPPENING. TOKEN SAID WAS ADMIN BUT NOT LISTED IN DATABASE 40 | console.warn("401: UNAUTHORIZED - User is not listed in database as admin") 41 | return done({message: "User is not listed in database as admin", code: "5-5"}, null) 42 | } 43 | } else { 44 | console.warn("401: UNAUTHORIZED - Admin list doesn't exit in database") 45 | return done({message: "Admin list doesn't exist in database", code: "5-21"},null) 46 | } 47 | }) 48 | } else { 49 | console.warn("401: UNAUTHORIZED - User is not an admin") 50 | return done({message: "User is not admin", code: 5}, null) 51 | } 52 | })); 53 | 54 | module.exports = passport; -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notes-server", 3 | "version": "1.0.0", 4 | "description": "Code for QuantEcon's Notes API", 5 | "private": true, 6 | "main": "./app.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start-server": "node app.js", 10 | "start-client": "cd client && npm start", 11 | "start:dev": "concurrently \"npm run start-server\" \"npm run start-client\"", 12 | "start-dev": "nodemon app.js", 13 | "start": "node app.js", 14 | "install-all": "npm install && npm run install-client", 15 | "install-client": "cd client && npm install", 16 | "build-client": "cd client && npm build", 17 | "api-docs": "apidoc -f \".*\\.js$\" -i ./routes -o ./docs", 18 | "client-docs": "jsdoc -r -d ./client/docs -c ./jsdoc-config.json", 19 | "setup-css": "cd client/src/assets && npm install --global gulp-cli && npm install && gulp", 20 | "migrate": "cd ./js/db && migrate" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/jstac/QuantEconLib.git" 25 | }, 26 | "author": "@tlyon3", 27 | "license": "BSD-3", 28 | "bugs": { 29 | "url": "https://github.com/jstac/QuantEconLib/issues" 30 | }, 31 | "homepage": "https://github.com/jstac/QuantEconLib#readme", 32 | "dependencies": { 33 | "async": "^2.4.1", 34 | "body-parser": "^1.18.2", 35 | "compression": "^1.7.2", 36 | "connect-multiparty": "^2.0.0", 37 | "cors": "^2.8.4", 38 | "email-validator": "^2.0.4", 39 | "express": "^4.16.2", 40 | "express-handlebars": "^3.0.0", 41 | "express-passport-logout": "^0.1.0", 42 | "express-session": "^1.15.6", 43 | "fuzzy-time": "^1.0.7", 44 | "jsonwebtoken": "^8.x.x", 45 | "mailgun-js": "^0.13.1", 46 | "migrate": "^1.6.2", 47 | "mongodb": "^2.2.33", 48 | "mongoose": "^5.0.12", 49 | "mongoose-paginate": "^5.0.3", 50 | "multer": "^1.3.0", 51 | "multiparty": "^4.1.3", 52 | "mustache": "^2.3.0", 53 | "node-persist": "^3.0.3", 54 | "normalize.css": "^7.0.0", 55 | "passport": "^0.4.x", 56 | "passport-facebook": "^2.1.1", 57 | "passport-github": "^1.1.0", 58 | "passport-google-oauth2": "^0.1.6", 59 | "passport-jwt": "^3.x.x", 60 | "passport-linkedin-oauth2": "^1.5.0", 61 | "passport-twitter": "^1.0.4", 62 | "passport-youtube-v3": "^2.0.0", 63 | "query-string": "^5.x.x", 64 | "react-jupyter": "^0.1.0", 65 | "sitemap": "^2.1.0", 66 | "sprintf": "^0.1.5", 67 | "tmp": "0.0.33" 68 | }, 69 | "apidoc": { 70 | "title": "Notes-Documentation", 71 | "description": "API documentation for Notes site", 72 | "url": "http://notes.quantecon.org", 73 | "order": [ 74 | "Information", 75 | "Search", 76 | "Submit", 77 | "Edit", 78 | "Delete", 79 | "Vote", 80 | "Edit_Profile", 81 | "Authentication" 82 | ] 83 | }, 84 | "devDependencies": { 85 | "jsdoc-jsx": "^0.1.0" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /client/src/containers/PreviewContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux' 3 | import {Redirect} from 'react-router-dom'; 4 | import {bindActionCreators} from 'redux' 5 | 6 | // import {bindActionCreators} from 'redux'; 7 | import Preview from '../components/submit/Preview' 8 | import {confirm, cancel} from '../actions/submit'; 9 | 10 | class PreviewContainer extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.onCancel = this 15 | .onCancel 16 | .bind(this); 17 | this.onSubmit = this 18 | .onSubmit 19 | .bind(this); 20 | } 21 | 22 | onCancel() { 23 | if (this.props.actions.cancel()) { 24 | this 25 | .props 26 | .history 27 | .push('/'); 28 | } else { 29 | console.log('[PreviewContainer] - error cancelling submission') 30 | } 31 | } 32 | 33 | onSubmit() { 34 | //TODO: this isn't waiting for return from API. 35 | var id = this 36 | .props 37 | .actions 38 | .confirm(); 39 | if (id) { 40 | console.log('[PreviewContainer] - success submitting'); 41 | this 42 | .props 43 | .history 44 | .replace('/'); 45 | } else { 46 | console.log('[PreviewContainer] - error confirming submission'); 47 | this.props.history.replace('/') 48 | } 49 | } 50 | 51 | componentWillReceiveProps(props) { 52 | console.log('[PreviewContainer] - new props: ', props); 53 | } 54 | render() { 55 | return ( 56 | 57 |
    58 | {this.props.submission 59 | ? 65 | : } 72 |
    73 | ) 74 | } 75 | } 76 | 77 | function mapStateToProps(state, props) { 78 | return {author: state.auth.user, submission: state.auth.user.currentSubmission, isLoading: state.auth.user.currentSubmission ? state.auth.user.currentSubmission.isLoading : true} 79 | } 80 | 81 | function mapDispatchToProps(dispatch) { 82 | return { 83 | actions: bindActionCreators({ 84 | confirm, 85 | cancel 86 | }, dispatch) 87 | } 88 | } 89 | 90 | export default connect(mapStateToProps, mapDispatchToProps)(PreviewContainer); -------------------------------------------------------------------------------- /client/src/assets/scss/src/helpers/_qe-menubar.scss: -------------------------------------------------------------------------------- 1 | // QuantEcon menubar styles 2 | 3 | .qe-menubar { 4 | border-bottom:1px solid #ccc; 5 | background:#fff; 6 | padding:2px 25px; 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | } 11 | .qe-menubar-logo { 12 | margin:0; 13 | font-family: 'Exo', sans-serif; 14 | font-weight:200; 15 | font-size:15px; 16 | position: relative; 17 | top:2px; 18 | } 19 | .qe-menubar-logo a { 20 | color: #000; 21 | text-decoration: none; 22 | font-weight: 200; 23 | } 24 | .qe-menubar-logo img { 25 | position: relative; 26 | top: -2px; 27 | margin: 0 5px 0 0; 28 | width:47px; 29 | } 30 | .qe-menubar-logo span { 31 | font-weight: 400; 32 | } 33 | .qe-menubar-nav { 34 | list-style: none; 35 | padding:0; 36 | margin:0; 37 | font-family: $body-font-family; 38 | font-size: 12px; 39 | display: flex; 40 | justify-content: space-between; 41 | } 42 | .qe-menubar-nav li { 43 | margin:0 0 0 15px; 44 | padding:0; 45 | } 46 | .qe-menubar-nav li a { 47 | color: #000; 48 | font-weight: normal; 49 | text-decoration: none; 50 | position: relative; 51 | padding: 0 15px 0 20px; 52 | } 53 | .qe-menubar-nav li:last-child a { 54 | padding-right:0; 55 | } 56 | .qe-menubar-nav li a:before { 57 | content: " "; 58 | width:16px; 59 | height:16px; 60 | position: absolute; 61 | left: 0; 62 | top: -1px; 63 | background:url(../img/qe-menubar-icons.png) no-repeat left top; 64 | background-size: 144px 16px; 65 | } 66 | .qe-menubar-nav li a:after { 67 | position: absolute; 68 | content: '\2022'; 69 | top:0; 70 | right:0; 71 | font-family: sans-serif; 72 | } 73 | .qe-menubar-nav li:last-child a:after { 74 | content:""; 75 | } 76 | .qe-menubar-nav li:nth-child(1) a:before { 77 | background-position: 0 0; 78 | } 79 | .qe-menubar-nav li:nth-child(2) a:before { 80 | background-position: -16px 0; 81 | } 82 | .qe-menubar-nav li:nth-child(3) a:before { 83 | background-position: -32px 0; 84 | } 85 | .qe-menubar-nav li:nth-child(4) a:before { 86 | background-position: -48px 0; 87 | } 88 | .qe-menubar-nav li:nth-child(5) a:before { 89 | background-position: -64px 0; 90 | } 91 | .qe-menubar-nav li:nth-child(6) a:before { 92 | background-position: -80px 0; 93 | } 94 | .qe-menubar-nav li:nth-child(7) a:before { 95 | background-position: -96px 0; 96 | } 97 | .qe-menubar-nav li:nth-child(8) a:before { 98 | background-position: -112px 0; 99 | } 100 | .qe-menubar-nav li:nth-child(9) a:before { 101 | background-position: -128px 0; 102 | } 103 | .show-for-sr { 104 | position: absolute!important; 105 | width: 1px; 106 | height: 1px; 107 | overflow: hidden; 108 | clip: rect(0,0,0,0); 109 | } 110 | 111 | @media (max-width: 1024px) { 112 | .qe-menubar { 113 | justify-content: center; 114 | } 115 | .qe-menubar-nav { 116 | display:none; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /client/src/components/admin/RemoveSubmissionModal.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import Modal from 'react-modal' 3 | 4 | import SubmissionPreview from '../submissions/submissionPreview' 5 | 6 | class RemoveSubmissionModal extends Component { 7 | constructor(props){ 8 | super(props) 9 | 10 | this.state = { 11 | textEntry: "" 12 | } 13 | 14 | this.textChanged = this.textChanged.bind(this) 15 | this.remove = this.remove.bind(this) 16 | } 17 | 18 | textChanged = (e) => { 19 | if(e){ 20 | e.preventDefault() 21 | } 22 | this.setState({ 23 | textEntry: e.target.value 24 | }) 25 | } 26 | 27 | remove = (e) => { 28 | console.log("Delete clicked") 29 | if(e){ 30 | e.preventDefault() 31 | } 32 | 33 | if(this.state.textEntry === this.props.submission.data.title){ 34 | this.props.onRemove(this.props.submission.data._id) 35 | } 36 | } 37 | 38 | render(){ 39 | return ( 40 | 44 |
    45 |
    46 |

    Remove Submission

    47 |
    48 | 49 |
    50 |

    Are you sure you want to remove this submission?

    51 |

    WARNING: THIS IS IRREVERSIBLE. THIS WILL REMOVE THE CONTENT FROM THE DATABASE

    52 | 53 |

    Enter the name of the submission to continue:

    54 |
    55 | 60 |
      61 |
    • 62 | 63 |
    • 64 |
    • 65 | 66 |
    • 67 |
    68 |
    69 |
    70 |
    71 |
    72 | 73 | ) 74 | } 75 | } 76 | 77 | export default RemoveSubmissionModal -------------------------------------------------------------------------------- /client/src/components/admin/AddAdminModal.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | // import UserPreview from '../user/UserPreview' 3 | import Modal from 'react-modal' 4 | import UserPreview from '../user/UserPreview'; 5 | 6 | class AddAdminModal extends Component { 7 | constructor(props){ 8 | super(props) 9 | 10 | this.search = this.search.bind(this) 11 | this.searchChanged = this.searchChanged.bind(this) 12 | this.makeAdminClicked = this.makeAdminClicked.bind(this) 13 | } 14 | 15 | componentWillReceiveProps(props){ 16 | console.log("[AddAdminModal] - will receive props: ", props) 17 | } 18 | 19 | search = (e) => { 20 | if(e){ 21 | e.preventDefault() 22 | this.props.onSearch(this.state.searchWords) 23 | } 24 | } 25 | 26 | searchChanged = (e) => { 27 | if(e) { 28 | e.preventDefault() 29 | this.setState({searchWords: e.target.value}) 30 | } 31 | } 32 | 33 | makeAdminClicked = (userID) => { 34 | this.props.makeAdmin(userID) 35 | } 36 | 37 | render() { 38 | return ( 39 | 43 |
    44 | 45 |
    46 |

    Add Admin

    47 |
    48 | 49 |
    50 | {/* Searchbar */} 51 |
    52 |
    53 | 58 |
    59 |
    60 | {/* List of users */} 61 | {this.props.searchResults 62 | ?
    63 | {this.props.searchResults.users.map((user, index) => { 64 | return
    65 | 66 |
      67 |
    • 68 | 69 |
    • 70 |
    71 |
    72 | })} 73 |
    74 | : null} 75 |
    76 | 77 |
      78 |
    • 79 | 80 |
    • 81 |
    82 |
    83 |
    84 | ) 85 | } 86 | } 87 | 88 | export default AddAdminModal -------------------------------------------------------------------------------- /client/src/containers/submission/SubmissionListContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux' 4 | import SubmissionList from '../../components/submissions/SubmissionList'; 5 | import * as SubmissionListActions from '../../actions/submissionList'; 6 | 7 | class SubmissionListContainer extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | searchParams: Object.assign({}, props.searchParams, props.searchP) 12 | } 13 | this.onSearch=this.onSearch.bind(this); 14 | } 15 | 16 | onSearch = (searchParams) => { 17 | this.setState({ 18 | searchParams: { ...searchParams, page: 1 } 19 | }) 20 | this.props.actions.fetchSubmissions({searchParams, forced: true}); 21 | } 22 | render() { 23 | return ( 24 |
    25 | 45 |
    46 | ) 47 | } 48 | } 49 | 50 | function mapStateToProps(state, props) { 51 | var searchParams = {}; 52 | if(props.resetSearch){ 53 | searchParams = { 54 | lang: 'All', 55 | time: 'All time', 56 | topic: 'All', 57 | author: '', 58 | keywords: '', 59 | page: 1, 60 | sortBy: 'Discover' 61 | } 62 | } else { 63 | searchParams = Object.assign({}, { 64 | lang: 'All', 65 | time: 'All time', 66 | topic: 'All', 67 | author: '', 68 | keywords: '', 69 | page: 1, 70 | sortBy: 'Discover' 71 | }, state.submissionList.searchParams); 72 | } 73 | if (props.userID) { 74 | searchParams = Object.assign(searchParams, {author: props.userID}); 75 | } else { 76 | if (searchParams.author) { 77 | delete searchParams.author; 78 | } 79 | 80 | } 81 | return {languages: state.submissionList.languages, searchParams: searchParams, submissionPreviews: state.submissionList.previews, totalSubmissions: state.submissionList.totalSubmissions, authors: state.submissionList.authors, isLoading: state.submissionList.isFetching} 82 | } 83 | 84 | function mapDispatchToProps(dispatch) { 85 | return { 86 | actions: bindActionCreators(SubmissionListActions, dispatch) 87 | } 88 | } 89 | 90 | export default connect(mapStateToProps, mapDispatchToProps)(SubmissionListContainer); 91 | -------------------------------------------------------------------------------- /client/src/components/App/App.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {BrowserRouter, Route, Switch} from 'react-router-dom'; 3 | import {reauthenticate} from '../../actions/auth/signIn' 4 | import {connect} from 'react-redux' 5 | import {bindActionCreators} from 'redux' 6 | 7 | // Containers 8 | import SubmissionContainer from '../../containers/submission/SubmissionContainer'; 9 | import UserContainer from '../../containers/user/UserContainer' 10 | import MyProfileContainer from '../../containers/user/MyProfileContainer' 11 | import SubmitContainer from '../../containers/SubmitContainer' 12 | import PreviewContainer from '../../containers/PreviewContainer' 13 | import EditProfileContainer from '../../containers/user/EditProfileContainer' 14 | import EditSubmissionContainer from '../../containers/submission/EditSubmissionContainer'; 15 | import AdminContainer from '../../containers/admin/AdminContainer' 16 | import Contact from '../Contact' 17 | //Components 18 | import Home from '../home/Home'; 19 | import ProtectedRoute from '../ProtectedRoute'; 20 | import SignIn from '../signin/SignIn'; 21 | import FAQ from '../FAQ'; 22 | import About from '../About'; 23 | import TempComponent from '../TempComponent' 24 | import NotFound from '../NotFound' 25 | import Sitemap from '../Sitemap' 26 | 27 | import '../../assets/css/main.css' 28 | //import '../../assets/css/app.css' 29 | //import '../../assets/css/general.css' 30 | //import '../../assets/css/formStyle.css' 31 | //import '../../assets/css/modal.css' 32 | 33 | class App extends Component { 34 | 35 | constructor(props){ 36 | super(props); 37 | 38 | props.actions.reauthenticate(window.location.href); 39 | } 40 | 41 | render() { 42 | return ( 43 |
    44 | 45 |
    46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
    64 |
    65 |
    66 | ); 67 | } 68 | } 69 | 70 | const mapDispatchToProps = (dispatch) => { 71 | return { 72 | actions: bindActionCreators({reauthenticate}, dispatch) 73 | } 74 | } 75 | 76 | const mapStateToProps = (state, props) => { 77 | return { 78 | isSignedIn: state.auth.isSignedIn, 79 | isLoading: state.auth.loading 80 | } 81 | } 82 | 83 | export default connect(mapStateToProps, mapDispatchToProps)(App); 84 | -------------------------------------------------------------------------------- /server/routes/auth/fb.js: -------------------------------------------------------------------------------- 1 | // var passport = require('../../js/auth/facebook'); 2 | // var express = require('express'); 3 | // var isAuthenticated = require('./isAuthenticated').isAuthenticated; 4 | // const qs = require('query-string'); 5 | // const User = require('../../js/db/models/User'); 6 | // const jwt = require('jsonwebtoken'); 7 | // const jwtAuth = require('../../js/auth/jwt'); 8 | // const AdminList = require('../../js/db/models/AdminList') 9 | 10 | // const appConfig = require('../../_config') 11 | 12 | // const select = 'name views numComments joinDate voteScore position submissions upvotes downvotes' + 13 | // ' avatar website email summary activeAvatar currentProvider github fb twitter google oneSocial new' 14 | 15 | // var app = express.Router(); 16 | 17 | // app.get('/add', jwtAuth.authenticate('jwt', { 18 | // session: false 19 | // }), passport.authenticate('facebook', { 20 | // scope: 'email' 21 | // })); 22 | 23 | // app.options('/', function (req, rex) { 24 | // console.log('in options for fb auth'); 25 | // }) 26 | 27 | 28 | // app.get('/', passport.authenticate('facebook', { 29 | // scope: 'email' 30 | // })); 31 | 32 | // app.get('/callback', passport.authenticate('facebook', { 33 | // failureRedirect: '/auth/failure' 34 | // }), function (req, res) { 35 | // User.findOne({ 36 | // '_id': req.user._id 37 | // }, select, function (err, user) { 38 | // if (err) { 39 | // res.status(500); 40 | // res.send({ 41 | // error: err 42 | // }); 43 | // } else if (user) { 44 | // AdminList.findOne({}, (err, adminList) => { 45 | // var token = jwt.sign({ 46 | // user: { 47 | // _id: user._id 48 | // } 49 | // }, "banana horse laser muffin"); 50 | 51 | // if (!err && adminList && adminList.adminIDs.indexOf(user._id) != -1) { 52 | // console.log("User is admin") 53 | // token = adminToken({ 54 | // user: { 55 | // _id: user._id 56 | // }, 57 | // isAdmin: true 58 | // }) 59 | // } 60 | 61 | // user.currentProvider = 'Facebook'; 62 | // var queryString = qs.stringify({ 63 | // token, 64 | // uid: req.user._id, 65 | // fromAPI: true 66 | // }); 67 | 68 | // user 69 | // .save(function (err) { 70 | // if (err) { 71 | // res.sendStatus(500); 72 | // } else { 73 | // const redirect = appConfig.redirectURL + "?" + queryString 74 | // console.log("[Google Auth] - redirect: ", redirect) 75 | // res.redirect(redirect); 76 | // } 77 | // }) 78 | // }) 79 | // } else { 80 | // res.status(500); 81 | // res.send({ 82 | // error: 'No user found' 83 | // }); 84 | // } 85 | 86 | // }); 87 | // }); 88 | 89 | // const adminToken = (data) => { 90 | // var token = jwt.sign(data, "banana horse laser muffin") 91 | // return token 92 | // } 93 | 94 | // module.exports = app; -------------------------------------------------------------------------------- /server/assets/aboutPage.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | QuantEcon Notes is a public repository that hosts Jupyter notebooks related to 4 | all areas of economics and econometrics. 5 | 6 | **Anyone can submit a notebook on either of these topics.** 7 | 8 | We encourage submissions containing either original material or replications 9 | of existing studies in an open and reproducible manner. Peer review is 10 | conducted purely by the free market, in the form of upvotes and downvotes. 11 | 12 | Our objective is to facilitate communication between economists and enhance 13 | reproducibility in research. 14 | 15 | URLs to individual notebooks will be permanent and can be used in citations. 16 | 17 | QuantEcon Notes is a [QuantEcon](https://quantecon.org) project funded by 18 | the [Alfred P. Sloan Foundation](https://sloan.org/). 19 | 20 | - - - 21 | 22 | # Project Team 23 | 24 | 25 | #### **Lead Developers** 26 | 27 | * Trevor Lyon 28 | 29 | * Aakash Gupta Choudhury 30 | 31 | #### **Prototype and Front-end Design** 32 | 33 | * Andrij Stachurski 34 | 35 | #### **Project Coordinators** 36 | 37 | * Chase Coleman 38 | 39 | * Spencer Lyon 40 | 41 | * Matthew McKay 42 | 43 | * John Stachurski 44 | 45 | * Natasha Watkins 46 | 47 | - - - 48 | 49 | # FAQs 50 | 51 | 1. **Can you handle supporting files?** 52 | 53 | Not yet, but we're getting there. 54 | 55 | 2. **Is this site only for economics?** 56 | 57 | Yes, with room for some finance or operations research if it closely relates 58 | to economics. Please downvote or [alert us](mailto:contact@quantecon.org) to content that you think is off 59 | topic. 60 | 61 | 3. **What should I do if I find an issue with the website?** 62 | 63 | [Contact us](mailto:contact@quantecon.org) 64 | 65 | 4. **How do I include an image in my Jupyter notebook?** 66 | 67 | Currently we don't support including additional files. If you'd like to link an image in a Markdown cell, you will need to host the image somewhere (refer [here](https://commonmark.org/help/) for Markdown syntax). 68 | If you would like to include an image without hosting, we recommend using `from IPython.display import Image`. 69 | 70 | - - - 71 | 72 | # License and Disclaimer 73 | 74 | THE SOFTWARE ON THIS SITE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED 75 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 76 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 77 | EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 78 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 79 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 80 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 81 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 82 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 83 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 84 | 85 | Software published on this site will be understood as having been released 86 | under the 3 Clause BSD license. 87 | 88 | - - - 89 | 90 | ## Provided Jupyter Notebook and Content 91 | 92 | Jupyter notebooks uploaded to this site will be considered to be released as 93 | [CC BY-ND 4.0 International](https://creativecommons.org/licenses/by-nd/4.0/). 94 | Users may copy and redistribute the material so long as attribution is 95 | provided to the original author of the notebook, and users may not remix, 96 | transform, or build upon the material. 97 | -------------------------------------------------------------------------------- /client/src/containers/user/EditProfileContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux' 3 | import {bindActionCreators} from 'redux' 4 | import {removeSocialAccount, setActiveAvatar, toggleSocial, editProfile} from '../../actions/auth/auth'; 5 | import {mergeAccounts} from '../../actions/auth/signIn' 6 | import EditProfile from '../../components/user/EditProfile' 7 | import * as AuthActions from '../../actions/auth/signOut' 8 | 9 | class EditProfileContainer extends Component { 10 | saveProfile = ({name, website, summary, email, emailSettings}) => { 11 | this 12 | .props 13 | .actions 14 | .editProfile({name, website, summary, email, emailSettings}); 15 | } 16 | 17 | cancel = () => { 18 | console.log('[EditProfileContainer] - cancel') 19 | this 20 | .props 21 | .history 22 | .push('/user/my-profile'); 23 | } 24 | 25 | toggleSocial = ({social}) => { 26 | console.log('[EditProfileContainer] - toggle social: ', social); 27 | this 28 | .props 29 | .actions 30 | .toggleSocial({social}); 31 | } 32 | 33 | setAvatar = ({social}) => { 34 | this 35 | .props 36 | .actions 37 | .setActiveAvatar({social}); 38 | } 39 | 40 | removeSocial = ({social}) => { 41 | this 42 | .props 43 | .actions 44 | .removeSocialAccount({social}); 45 | } 46 | 47 | mergeAccounts = ({accountToMerge, next}) => { 48 | this.props.actions.mergeAccounts({ 49 | accountToMerge, 50 | next 51 | }) 52 | } 53 | 54 | componentWillReceiveProps(props){ 55 | if(!props.loading && !props.isSignedIn){ 56 | props.history.push('/signin') 57 | } 58 | } 59 | 60 | render() { 61 | return ( 62 |
    63 | {this.props.loading 64 | ? "loading..." 65 | : } 77 |
    78 | ) 79 | } 80 | } 81 | 82 | function mapStateToProps(state, props) { 83 | return { 84 | user: state.auth.user, 85 | editProfileError: state.auth.editProfileError, 86 | editProfileSuccess: state.auth.editProfileSuccess, 87 | loading: state.auth.loading, 88 | isSignedIn: state.auth.isSignedIn 89 | } 90 | } 91 | 92 | function mapDispatchToProps(dispatch) { 93 | return { 94 | actions: bindActionCreators({ 95 | removeSocialAccount, 96 | setActiveAvatar, 97 | toggleSocial, 98 | editProfile, 99 | mergeAccounts, 100 | ...AuthActions, 101 | }, dispatch) 102 | } 103 | } 104 | 105 | export default connect(mapStateToProps, mapDispatchToProps)(EditProfileContainer); 106 | -------------------------------------------------------------------------------- /client/src/containers/submission/EditSubmissionContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types' 3 | import {connect} from 'react-redux'; 4 | import {bindActionCreators} from 'redux' 5 | import EditSubmission from '../../components/submissions/EditSubmission' 6 | import {editSubmission} from '../../actions/submission' 7 | 8 | /** 9 | * Parent container for the {@link EditSubmission} component. Retrieves data and actions 10 | * from the Redux store and passes to the child component 11 | */ 12 | class EditSubmissionContainer extends Component { 13 | /** 14 | * @prop {Object} currentUser Object containing the current user's information. If no 15 | * user is signed in, will be `null` 16 | * @prop {Object} submission Object containing all the information of the submission 17 | * @prop {Object} actions Contains all actions necessary for editing a submission 18 | */ 19 | static propTypes = { 20 | currentUser: PropTypes.object.isRequired, 21 | submission: PropTypes.object.isRequired, 22 | history: PropTypes.object.isRequired, 23 | } 24 | 25 | constructor(props) { 26 | super(props); 27 | console.log("esc props:" , props) 28 | 29 | this.save = this 30 | .save 31 | .bind(this); 32 | this.saveCallback = this.saveCallback.bind(this) 33 | } 34 | 35 | /** 36 | * Dispatches a save action. Once the action completes, `saveCallback` will be called. 37 | * 38 | * Note: Either the `file` or `notebookJSON` will be null. If the user uploaded a new file, 39 | * `notebookJSON` will be null. If no new file was uploaded, `file` will be null and `notebookJSON` 40 | * will be populated with the original contents of the ipynb file. 41 | * 42 | * @param {Object} param0 43 | * @param {Object} param0.formData Contains all the information the user entered in the form 44 | * @param {File} param0.file Reference to the file the user uploaded. 45 | * @param {Object} param0.notebookJSON JSON representing the ipynb file 46 | */ 47 | save ({formData, file, notebookJSON}) { 48 | 49 | this 50 | .props 51 | .actions 52 | .editSubmission({formData, file, notebookJSON, submissionID: this.props.match.params.id}, this.saveCallback); 53 | } 54 | 55 | /** 56 | * Callback for a save action. If there was an error saving the submission, `success` will 57 | * be false, otherwise it will be true. 58 | * @param {bool} success Save was successful or not 59 | */ 60 | saveCallback(success) { 61 | if(success){ 62 | this.props.history.push('/submission/' + this.props.match.params.id) 63 | } else { 64 | console.warn("Error editing submission") 65 | } 66 | } 67 | 68 | render() { 69 | return ( 70 |
    71 | 76 |
    77 | ) 78 | } 79 | } 80 | 81 | const mapStateToProps = (state, props) => { 82 | const submissionID = props.match.params.id; 83 | return {submission: state.submissionByID[submissionID], currentUser: state.auth.user} 84 | } 85 | 86 | const mapDispatchToProps = (dispatch) => { 87 | return { 88 | actions: bindActionCreators({editSubmission}, dispatch) 89 | } 90 | } 91 | 92 | export default connect(mapStateToProps, mapDispatchToProps)(EditSubmissionContainer); 93 | -------------------------------------------------------------------------------- /client/src/components/home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | // import SubmissionList from '../submissions/SubmissionList'; 4 | import SubmissionListContainer from '../../containers/submission/SubmissionListContainer'; 5 | import HeadContainer from '../../containers/HeadContainer'; 6 | import AnnouncementsContainer from '../../containers/AnnouncementsContainer' 7 | 8 | // import Image component and json of image data details 9 | import Image from '../Image.jsx'; 10 | // import data from '../../imageData.json'; 11 | import uuid from 'uuid'; 12 | 13 | import sloanLogo from '../../assets/img/logo/landing-sloan-logo.png'; 14 | 15 | class Home extends Component { 16 | constructor(props){ 17 | super(props); 18 | 19 | var resetSearch = false 20 | var searchParams = this.getUrlVars(window.location.href); 21 | if(searchParams.reset) { 22 | resetSearch = true 23 | } 24 | if(searchParams.topic){ 25 | searchParams.topic = decodeURIComponent(searchParams.topic); 26 | } 27 | console.log('[Home] - url search params: ', searchParams); 28 | 29 | this.state = { 30 | searchParams: searchParams, 31 | reset: resetSearch 32 | } 33 | } 34 | 35 | getUrlVars = () => { 36 | var vars = [], hash; 37 | var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); 38 | for(var i = 0; i < hashes.length; i++) 39 | { 40 | hash = hashes[i].split('='); 41 | vars[hash[0]] = hash[1]; 42 | } 43 | return vars; 44 | } 45 | 46 | createImage = (image) => { 47 | // Temporarily using uuid() for now as unique keys 48 | return ; 49 | } 50 | 51 | createImages = (images) => { 52 | return images.map(this.createImage); 53 | } 54 | 55 | render() { 56 | return ( 57 |
    58 | 59 | 60 |
    61 |
    62 |

    An open Jupyter notebook library for economics and finance

    63 | Sloan Logo 64 | {/*
      65 | {this.createImages(data.images)} 66 |
    */} 67 |
    68 |
    69 | 70 | 71 | 72 | {/* */} 73 | 74 | 75 | {/*
    76 |
    77 |
      78 |
    • QuantEcon Logo
    • 79 |
    • Jupyter Logo
    • 80 |
    • Sloan Logo
    • 81 |
    82 |
    83 |
    */} 84 |
    85 | 86 | ); 87 | } 88 | componentDidMount(){ 89 | document.title = 'QuantEcon - Notes' 90 | } 91 | } 92 | 93 | export default Home; 94 | -------------------------------------------------------------------------------- /client/src/assets/scss/src/misc.scss: -------------------------------------------------------------------------------- 1 | // misc.scss 2 | // Miscellaneous styles 3 | 4 | .success-checkmark { 5 | margin: 0rem 1rem; 6 | } 7 | 8 | .email-error { 9 | text-align: center; 10 | color: #C95627; 11 | margin-top: 1em; 12 | } 13 | 14 | .invite-label { 15 | color: #d84d0a !important; 16 | } 17 | 18 | .loading-spinner { 19 | margin: auto; 20 | display: block; 21 | 22 | } 23 | 24 | .loading-container { 25 | margin-left: auto; 26 | margin-right: auto; 27 | height: 100%; 28 | } 29 | 30 | .overlay { 31 | background: rgba(0, 0, 0, .4); 32 | position: fixed; 33 | left: 0; 34 | top: 0; 35 | } 36 | 37 | p.word-count{ 38 | display: inline-block; 39 | margin-left: 250px; 40 | } 41 | 42 | span.words { 43 | font-weight: bold; 44 | color: #d84d0a; 45 | } 46 | 47 | ul.links { 48 | list-style-type: none; 49 | } 50 | 51 | li.link { 52 | display: inline-block; 53 | padding-top: .5rem; 54 | padding-right:1rem; 55 | padding-left: 1rem; 56 | } 57 | 58 | ul.terms-and-conditions { 59 | list-style-type: none; 60 | } 61 | 62 | .dot { 63 | width: 3px; 64 | height: 3px; 65 | background-color: #888888; 66 | border-radius: 50%; 67 | display: inline-block; 68 | margin-bottom: 2px; 69 | margin-left: 6px; 70 | margin-right: 6px; 71 | } 72 | 73 | span.title { 74 | font-weight: bold; 75 | } 76 | 77 | .edited-tag { 78 | margin-bottom: 0px; 79 | } 80 | 81 | .comment-body .edited-tag { 82 | color: #888; 83 | } 84 | 85 | ul.pagination li.active { 86 | border: 1px solid #d84d0a; 87 | background: transparent; 88 | color: #d84d0a !important; 89 | font-weight: 700 90 | } 91 | 92 | ul.pagination li.active a { 93 | color: #d84d0a !important; 94 | font-weight: 700 95 | } 96 | 97 | img.text-loading { 98 | height: 10%; 99 | width: 10%; 100 | } 101 | 102 | li.vertical-center { 103 | padding-top: 1rem; 104 | } 105 | 106 | ul.horizontal { 107 | list-style: none; 108 | display: flex; 109 | justify-content: center; 110 | margin: 2rem 0 0; 111 | vertical-align: middle; 112 | } 113 | 114 | p.banner-center{ 115 | text-align: 'center'; 116 | float: left; 117 | width: 75% 118 | } 119 | 120 | img.text-loading-medium { 121 | height: 60%; 122 | width: 60%; 123 | } 124 | 125 | img.loading-avatar { 126 | height: 100%; 127 | width: 200%; 128 | } 129 | 130 | .question::before { 131 | content: 'Q: '; 132 | color: #d84d0a; 133 | font-size: 1.5rem; 134 | font-weight: bold; 135 | } 136 | 137 | .answer::before { 138 | content: 'A: '; 139 | color: #d84d0a; 140 | font-size: 1.5rem; 141 | font-weight: bold; 142 | } 143 | 144 | .border-bottom { 145 | border-bottom: 1px solid #cacaca; 146 | padding-bottom: 1rem; 147 | } 148 | 149 | .page-content { 150 | background: white; 151 | padding: 2rem 4rem; 152 | margin: 2rem 0; 153 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); 154 | font-size: 1.2rem; 155 | } 156 | 157 | .title-name { 158 | font-size: 2.6em; 159 | border-bottom: 1px solid #cacaca; 160 | color: #C95627; 161 | padding: 0.5rem 1.5rem; 162 | } 163 | 164 | /* Notebook rendering */ 165 | 166 | .notebook-preview .cell .outputs code { 167 | font-family: monospace !important; 168 | } 169 | 170 | .notebook-preview .cell .prompt { 171 | width: 30px !important; 172 | } 173 | 174 | .notebook-preview .content-margin { 175 | padding-left: 40px !important; 176 | } 177 | 178 | .ml-6 { 179 | margin-left: 6px; 180 | } 181 | -------------------------------------------------------------------------------- /client/src/components/FAQ.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import HeadContainer from '../containers/HeadContainer' 3 | import Breadcrumbs from './partials/Breadcrumbs'; 4 | 5 | //import '../assets/css/faq.css' 6 | 7 | class FAQ extends Component { 8 | render() { 9 | return ( 10 |
    11 | 12 | 13 |
    14 |
    15 |

    16 | Frequently Asked Questions 17 |

    18 |
    19 | 20 |
    21 |

    22 | How does this site work? 23 |

    24 |

    25 | This site is used to encourage the sharing and use of open source code for 26 | economic research and modeling. Users can upload their own work, find other 27 | notebooks, vote, comment and download notebooks. 28 |

    29 |
    30 |
    31 |

    32 | Are the notebooks interactive? 33 |

    34 |

    35 | Sort of. There is no kernel running, so you cannot execute code cells. However, 36 | libraries such as PlotlyJS that use JavaScript allow a notebook's output cells 37 | to be interactive. 38 |

    39 |
    40 |
    41 |

    42 | Is this site only for economics? 43 |

    44 |

    45 | The target audience is for economists and economic students and professors. 46 |

    47 |
    48 |
    49 |

    50 | Can anyone submit a notebook? 51 |

    52 |

    53 | Yes! We encourage you to share your work and discover others' work! 54 |

    55 |
    56 |
    57 |

    58 | What should I do if I find an issue with the website? 59 |

    60 |

    61 | Follow 62 | this link 63 | and post an issue. Please provide a detailed description of the issue and what 64 | you were doing at the time. Submit the issue under the category "Notes Feedback". 65 |

    66 |
    67 | 68 |
    69 |
    70 | ) 71 | } 72 | } 73 | 74 | export default FAQ; 75 | -------------------------------------------------------------------------------- /client/src/containers/submission/SubmissionContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types' 3 | import {connect} from 'react-redux'; 4 | import {bindActionCreators} from 'redux' 5 | import Submission from '../../components/submissions/Submission'; 6 | import * as AuthActions from '../../actions/auth/auth'; 7 | import {downvoteSubmission, upvoteSubmission} from '../../actions/auth/vote' 8 | import {fetchNBInfo, deleteSubmission, flagSubmission} from '../../actions/submission' 9 | import NotFound from '../../components/NotFound' 10 | 11 | const actions = { 12 | ...AuthActions, 13 | fetchNBInfo, 14 | downvoteSubmission, 15 | upvoteSubmission, 16 | deleteSubmission, 17 | flagSubmission 18 | } 19 | 20 | /** 21 | * Submission Container 22 | * 23 | * Parent container for {@link Submission}. Retrieves all data from the Redux store 24 | * and passes it to the child Submission component. 25 | */ 26 | class SubmissionContainer extends Component { 27 | /** 28 | * @prop {Object} actions Contains the actions required for the submission page. 29 | */ 30 | static propTypes = { 31 | actions: PropTypes.object.isRequired 32 | } 33 | 34 | constructor(props) { 35 | super(props); 36 | this 37 | .props 38 | .actions 39 | .fetchNBInfo({notebookID: props.match.params.id}); 40 | } 41 | 42 | render() { 43 | if(this.props.submission && this.props.submission.error){ 44 | return ( 45 |
    46 | 47 |
    48 | ) 49 | } else { 50 | return ( 51 |
    52 | 65 |
    66 | ) 67 | } 68 | 69 | 70 | } 71 | } 72 | 73 | function mapStateToProps(state, props) { 74 | var il = true; 75 | var nbLoading = true; 76 | var dataReceived = 0; 77 | var totalData = 10000; 78 | if (state.submissionByID[props.match.params.id]) { 79 | il = state.submissionByID[props.match.params.id].isFetching 80 | nbLoading = state.submissionByID[props.match.params.id].isFetchingNB 81 | dataReceived = state.submissionByID[props.match.params.id].dataReceived 82 | totalData = state.submissionByID[props.match.params.id].totalData 83 | } 84 | return { 85 | submission: state.submissionByID[props.match.params.id], 86 | currentUser: state.auth.isSignedIn 87 | ? state.auth.user 88 | : null, 89 | isLoading: il, 90 | nbLoading, 91 | dataReceived, 92 | totalData, 93 | isAdmin: state.auth.isAdmin, 94 | isSignedIn: state.auth.isSignedIn 95 | } 96 | } 97 | 98 | function mapDispatchToProps(dispatch) { 99 | return { 100 | actions: bindActionCreators(actions, dispatch) 101 | } 102 | } 103 | 104 | export default connect(mapStateToProps, mapDispatchToProps)(SubmissionContainer); 105 | -------------------------------------------------------------------------------- /client/src/components/submissions/SubmissionList.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Searchbar from '../searchbar/Searchbar'; 3 | import SubmissionPreview from './submissionPreview'; 4 | import Paginate from 'react-paginate' 5 | 6 | class SubmissionList extends Component { 7 | 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | submissionPreviews: [], 12 | searchParams: props.searchParams, 13 | currentPage: 0 14 | } 15 | } 16 | 17 | componentWillReceiveProps(props) { 18 | // console.log('[SubmissionList] - received new props: ', props); 19 | this.setState({submissionPreviews: props.submissionPreviews}) 20 | } 21 | 22 | onSearch = (searchParams) => { 23 | this.setState({ 24 | searchParams, 25 | currentPage: (searchParams.page - 1) 26 | }, () => { 27 | this.props.onSearch(searchParams); 28 | }) 29 | } 30 | onPageChange = (page) => { 31 | var newSearchParams = Object.assign({}, this.state.searchParams, { 32 | page: page.selected + 1 33 | }) 34 | this.onSearch(newSearchParams); 35 | } 36 | 37 | 38 | render() { 39 | return ( 40 |
    41 |
    42 | 48 |
    49 | {/*Repeat for each notebook*/} 50 | 51 | {this.props.isLoading 52 | ?

    loading...

    53 | :
    54 | {this 55 | .state 56 | .submissionPreviews 57 | .map((submission, index) => { 58 | //get author 59 | var author = this 60 | .props 61 | .authors 62 | .filter(function (a) { 63 | return a._id === submission.author; 64 | }); 65 | return 66 | }) 67 | } 68 |
    } 69 |
    70 | 85 |
    86 |
    87 | ); 88 | } 89 | }; 90 | 91 | export default SubmissionList; 92 | -------------------------------------------------------------------------------- /server/routes/auth/twitter.js: -------------------------------------------------------------------------------- 1 | var passport = require('../../js/auth/twitter'); 2 | var express = require('express'); 3 | var isAuthenticated = require('./isAuthenticated').isAuthenticated; 4 | const jwtAuth = require('../../js/auth/jwt'); 5 | const User = require('../../js/db/models/User'); 6 | const jwt = require('jsonwebtoken'); 7 | const qs = require('query-string'); 8 | const appConfig = require('../../_config') 9 | const AdminList = require('../../js/db/models/AdminList') 10 | 11 | var app = express.Router(); 12 | var referer = ''; 13 | // twitter login ========================== 14 | 15 | // add twitter to existing profile 16 | app.get('/add', jwtAuth.authenticate('jwt', { 17 | session: false 18 | }), passport.authenticate('twitter', { 19 | scope: 'email' 20 | })); 21 | 22 | // register/login with twitter 23 | /** 24 | * @api {get} /api/auth/fb Twitter 25 | * @apiGroup Authentication 26 | * @apiName AuthenticateTwitter 27 | * 28 | * @apiVersion 1.0.0 29 | * 30 | * @apiDescription API endpoint for Twitter OAuth. The user is redirected to Twitter's OAuth 31 | * screen. 32 | * 33 | * On a successful authentication, the window will be redirected with a JSON Web Token in the url 34 | * parameters which the client uses for future authentication 35 | */ 36 | app.get('/', passport.authenticate('twitter', { 37 | scope: 'email' 38 | })); 39 | 40 | app.get('/callback', 41 | passport.authenticate('twitter', { 42 | failureRedirect: '/auth/failure' 43 | }), 44 | function (req, res) { 45 | console.log('[TwitterAuth] - after second auth'); 46 | 47 | const select = 'name views numComments joinDate voteScore position submissions upvotes downvotes' + 48 | ' avatar website email summary activeAvatar currentProvider github fb twitter google oneSocial new' 49 | User.findOne({ 50 | '_id': req.user._id 51 | }, select, function (err, user) { 52 | if (err) { 53 | console.log('[TwitterAuth] - error finding user') 54 | res.sendStatus(500); 55 | } else if (user) { 56 | AdminList.findOne({}, (err, adminList) => { 57 | var token = jwt.sign({ 58 | user: { 59 | _id: user._id 60 | } 61 | }, "banana horse laser muffin"); 62 | 63 | if (!err && adminList && adminList.adminIDs.indexOf(user._id) != -1) { 64 | console.log("User is admin") 65 | token = adminToken({ 66 | user: {_id: user._id}, 67 | isAdmin: true 68 | }) 69 | } 70 | 71 | user.currentProvider = 'Twitter'; 72 | 73 | var queryString = qs.stringify({ 74 | token, 75 | uid: req.user._id, 76 | fromAPI: true 77 | }); 78 | 79 | user 80 | .save(function (err) { 81 | if (err) { 82 | res.sendStatus(500); 83 | } else { 84 | const redirect = appConfig.redirectURL + "?" + queryString 85 | console.log("[Google Auth] - redirect: ", redirect) 86 | res.redirect(redirect); 87 | } 88 | }) 89 | }) 90 | } else { 91 | console.log('[TwitterAuth] - no user found') 92 | res.sendStatus(500); 93 | } 94 | }); 95 | 96 | } 97 | ); 98 | 99 | const adminToken = (data) => { 100 | var token = jwt.sign(data, "banana horse laser muffin") 101 | return token 102 | } 103 | 104 | module.exports = app; -------------------------------------------------------------------------------- /client/src/actions/editSubmission.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Actions for editing a submission 3 | * @author Trevor Lyon 4 | * 5 | * @module editSubmissionActions 6 | */ 7 | 8 | import store from '../store/store' 9 | 10 | export const SAVE_SUBMISSION = 'SAVE_SUBMISSION'; 11 | const saveSubmissionAction = ({ 12 | submission 13 | }) => { 14 | return { 15 | type: SAVE_SUBMISSION, 16 | submission 17 | } 18 | } 19 | 20 | /** 21 | * @function 22 | * @name buildSubmissionPreview 23 | * 24 | * @description Builds a submission JSON object that is used for rendering in the modal that is displayed when the user clicks on the 25 | * Preview button. 26 | * 27 | * Note: only one of the parameters `file` and `notebookJSON` should be used. The other should be `null`. If the user supplies 28 | * a new file, then `file` will be used and `notebookJSON` should be `null`. If no new file is supplied, then the existing 29 | * `notebookJSON` is used and `file` should be `null` 30 | * 31 | * @param {Object} data 32 | * @param {Object} data.formData - A JSON Object of the data filled out by the form. This object should contain the following: 33 | * ``` 34 | * { 35 | * agreement: Boolean, 36 | * title: String, 37 | * summary: String, 38 | * lang: String, 39 | * topics: Array[String], 40 | * coAuthors: Array[String] 41 | * }``` 42 | * @param {File} data.file - The file of the notebook. This should be a file with an .ipynb file extension 43 | * @param {Object} data.notebookJSON - The JSON of the notebook 44 | * @param {String} data.submissionID - The ID of the submission being edited 45 | * 46 | * @returns {Object} The submission object that contains all data necessary to render a submission page 47 | */ 48 | export const buildSubmissionPreview = ({ 49 | formData, 50 | file, 51 | notebookJSON, 52 | submissionID 53 | }) => { 54 | 55 | var submission = { 56 | ...formData, 57 | lastUpdated: 9999999, 58 | _id: submissionID, 59 | author: store.getState().auth.user 60 | }; 61 | 62 | if (file) { 63 | //read and parse file 64 | var reader = new FileReader(); 65 | reader.readAsText(file); 66 | reader.onload = (event) => { 67 | submission.notebookJSON = JSON.parse(event.target.result); 68 | return submission 69 | } 70 | } else if (notebookJSON) { 71 | submission.notebookJSON = notebookJSON 72 | return submission 73 | } else { 74 | return { 75 | error: 'No notebook given' 76 | } 77 | } 78 | 79 | } 80 | 81 | /** 82 | * @function buildAndSave 83 | * @description REDUX ACTION: Builds the submission object and dispatches an action to save the changes 84 | * 85 | * @param {Object} data - Data used to build the submission preview. Contains `formData`, `file`, `notebookJSON`, `submissionID` 86 | * @param {Object} data.formData - A JSON Object of the data filled out by the form. This object should contain the following: 87 | * ``` 88 | * { 89 | * agreement: Boolean, 90 | * title: String, 91 | * summary: String, 92 | * lang: String, 93 | * topics: Array[String], 94 | * coAuthors: Array[String] 95 | * }``` 96 | * @param {File} data.file - The file of the notebook. This should be a file with an .ipynb file extension 97 | * @param {Object} data.notebookJSON - The JSON of the notebook. This is used incase the user didn't select a new file but is using 98 | * the existing file 99 | * @param {String} data.submissionID - The ID of the submission being edited 100 | */ 101 | export const buildAndSave = ({ 102 | formData, 103 | file, 104 | notebookJSON, 105 | submissionID 106 | }) => { 107 | return (dispatch) => { 108 | var submission = buildSubmissionPreview({formData, file, notebookJSON, submissionID}) 109 | if (submission.error){ 110 | dispatch(saveSubmissionAction({ 111 | error: 'No notebook given' 112 | })); 113 | } else { 114 | dispatch(saveSubmissionAction({submission})); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /client/src/assets/scss/src/helpers/_fonts.scss: -------------------------------------------------------------------------------- 1 | /* vietnamese */ 2 | @font-face { 3 | font-family: 'Exo'; 4 | font-display: block; 5 | font-style: normal; 6 | font-weight: 200; 7 | src: local('Exo ExtraLight'), local('Exo-ExtraLight'), url(https://fonts.gstatic.com/s/exo/v7/4UaDrEtFpBIavF-2-BLjza_B4qN1.woff2) format('woff2'); 8 | unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; 9 | } 10 | /* latin-ext */ 11 | @font-face { 12 | font-family: 'Exo'; 13 | font-display: block; 14 | font-style: normal; 15 | font-weight: 200; 16 | src: local('Exo ExtraLight'), local('Exo-ExtraLight'), url(https://fonts.gstatic.com/s/exo/v7/4UaDrEtFpBIavF-2-RLjza_B4qN1.woff2) format('woff2'); 17 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 18 | } 19 | /* latin */ 20 | @font-face { 21 | font-family: 'Exo'; 22 | font-display: block; 23 | font-style: normal; 24 | font-weight: 200; 25 | src: local('Exo ExtraLight'), local('Exo-ExtraLight'), url(https://fonts.gstatic.com/s/exo/v7/4UaDrEtFpBIavF-29xLjza_B4g.woff2) format('woff2'); 26 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 27 | } 28 | /* vietnamese */ 29 | @font-face { 30 | font-family: 'Exo'; 31 | font-display: block; 32 | font-style: normal; 33 | font-weight: 400; 34 | src: local('Exo Regular'), local('Exo-Regular'), url(https://fonts.gstatic.com/s/exo/v7/4UaOrEtFpBISfH6j2jDu55XI.woff2) format('woff2'); 35 | unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; 36 | } 37 | /* latin-ext */ 38 | @font-face { 39 | font-family: 'Exo'; 40 | font-display: block; 41 | font-style: normal; 42 | font-weight: 400; 43 | src: local('Exo Regular'), local('Exo-Regular'), url(https://fonts.gstatic.com/s/exo/v7/4UaOrEtFpBISfX6j2jDu55XI.woff2) format('woff2'); 44 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 45 | } 46 | /* latin */ 47 | @font-face { 48 | font-family: 'Exo'; 49 | font-display: block; 50 | font-style: normal; 51 | font-weight: 400; 52 | src: local('Exo Regular'), local('Exo-Regular'), url(https://fonts.gstatic.com/s/exo/v7/4UaOrEtFpBISc36j2jDu5w.woff2) format('woff2'); 53 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 54 | } 55 | /* latin-ext */ 56 | @font-face { 57 | font-family: 'Raleway'; 58 | font-display: block; 59 | font-style: normal; 60 | font-weight: 400; 61 | src: local('Raleway'), local('Raleway-Regular'), url(https://fonts.gstatic.com/s/raleway/v12/1Ptug8zYS_SKggPNyCMIT4ttDfCmxA.woff2) format('woff2'); 62 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 63 | } 64 | /* latin */ 65 | @font-face { 66 | font-family: 'Raleway'; 67 | font-display: block; 68 | font-style: normal; 69 | font-weight: 400; 70 | src: local('Raleway'), local('Raleway-Regular'), url(https://fonts.gstatic.com/s/raleway/v12/1Ptug8zYS_SKggPNyC0IT4ttDfA.woff2) format('woff2'); 71 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 72 | } 73 | /* latin-ext */ 74 | @font-face { 75 | font-family: 'Raleway'; 76 | font-display: block; 77 | font-style: normal; 78 | font-weight: 700; 79 | src: local('Raleway Bold'), local('Raleway-Bold'), url(https://fonts.gstatic.com/s/raleway/v12/1Ptrg8zYS_SKggPNwJYtWqhPANqczVsq4A.woff2) format('woff2'); 80 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 81 | } 82 | /* latin */ 83 | @font-face { 84 | font-family: 'Raleway'; 85 | font-display: block; 86 | font-style: normal; 87 | font-weight: 700; 88 | src: local('Raleway Bold'), local('Raleway-Bold'), url(https://fonts.gstatic.com/s/raleway/v12/1Ptrg8zYS_SKggPNwJYtWqZPANqczVs.woff2) format('woff2'); 89 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 90 | } -------------------------------------------------------------------------------- /client/src/actions/submissionList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Submission List actions 3 | * @author Trevor Lyon 4 | * 5 | * @module submissionListActions 6 | */ 7 | 8 | import queryString from 'query-string'; 9 | import store from '../store/store' 10 | 11 | export const REQUEST_SUBMISSION_PREVIEWS = 'REQUEST_SUBMISSION_PREVIEWS' 12 | export function requestSubmissionPreviews(searchParams = { 13 | lang: 'All', 14 | time: 'All time', 15 | topic: 'All', 16 | author: '', 17 | keywords: '', 18 | page: 1, 19 | sortBy: 'Discover' 20 | }) { 21 | return { 22 | type: REQUEST_SUBMISSION_PREVIEWS, 23 | searchParams 24 | } 25 | } 26 | 27 | export const RECEIVE_SUBMISSION_PREVIEWS = 'RECEIVE_SUBMISSION_PREVIEWS'; 28 | export function receiveSubmissionPreviews(searchParams, json) { 29 | return { 30 | type: RECEIVE_SUBMISSION_PREVIEWS, 31 | searchParams, 32 | submissionList: json.submissions, 33 | authors: json.authors, 34 | totalSubmissions: json.totalSubmissions, 35 | languages: json.languages, 36 | receivedAt: new Date() 37 | } 38 | } 39 | 40 | export const INVALIDATE_SUBMISSION_LIST = 'INVALIDATE_SUBMISSION_LIST'; 41 | export function invalidateSubmissionList() { 42 | return { 43 | type: INVALIDATE_SUBMISSION_LIST, 44 | 45 | } 46 | } 47 | 48 | export const RESET_SEARCH_PARAMS = "RESET_SEARCH_PARAMS"; 49 | export const resetSearchParamsAction = () => { 50 | return { 51 | type: RESET_SEARCH_PARAMS 52 | } 53 | } 54 | 55 | export const resetSearchParams = () => { 56 | return function(dispatch) { 57 | dispatch(resetSearchParamsAction()) 58 | } 59 | } 60 | 61 | //TODO: Dispatch on error action 62 | 63 | /** 64 | * @function fetchSubmissions 65 | * @description Makes an API request to get all submission that match the `searchParams` 66 | * @param {Object} param0 67 | * @param {Object} param0.searchParams Search params provided by the user 68 | * @param {bool} param0.forced Boolean flag to bypass the `needToFetch` check 69 | */ 70 | export const fetchSubmissions = ({searchParams, forced}) => { 71 | var sp = Object.assign({}, { 72 | lang: 'All', 73 | time: 'All time', 74 | topic: 'All', 75 | author: '', 76 | keywords: '', 77 | page: 1, 78 | sortBy: 'Discover' 79 | }, searchParams) 80 | console.log('[Search] - search params: ', sp); 81 | return function (dispatch) { 82 | if (forced || shouldFetchSummaries(store.getState(), sp)) { 83 | dispatch(requestSubmissionPreviews(sp)); 84 | var qs = queryString.stringify(sp); 85 | fetch('/api/search/all-submissions/?' + qs) 86 | .then((response) => { 87 | return response.json(); 88 | }, (error) => console.log('An error occurred: ', error)) 89 | .then(json => { 90 | dispatch(receiveSubmissionPreviews(sp, json)); 91 | }) 92 | } else { 93 | console.log('[FetchSubmissions] - don\'t need to fetch'); 94 | } 95 | } 96 | } 97 | 98 | export const shouldFetchSummaries = (state, searchParams) => { 99 | return true; 100 | // if (!state.submissionList) { 101 | // console.log('[ShouldFetch] - !state.submissionList') 102 | // return true; 103 | // } else if (state.isFetching) { 104 | // console.log('[ShouldFetch] - state.isFetching') 105 | // return false; 106 | // } else if (state.didInvalidate) { 107 | // console.log('[ShouldFetch] - state.didInvalidate') 108 | // return true; 109 | // } else if(JSON.stringify(searchParams) !== JSON.stringify(state.submissionList.searchParams)){ 110 | // console.log('[ShouldFetch] - search params not equal: '); 111 | // return true 112 | // } else if(new Date() > new Date(state.submissionList.lastUpdated.getTime() + 5*60000)){ 113 | // console.log('[ShouldFetch] - time expired'); 114 | // return true; 115 | // } else { 116 | // var expiresAt = new Date(state.submissionList.lastUpdated.getTime() + 5*60000); 117 | // console.log('[ShouldFetch] - expires at: ', expiresAt.getHours() + ':'+ expiresAt.getMinutes()); 118 | // return false; 119 | // } 120 | } 121 | -------------------------------------------------------------------------------- /client/src/assets/scss/src/layouts/listing.scss: -------------------------------------------------------------------------------- 1 | // Submission listing styles 2 | 3 | .filters { 4 | position: relative; 5 | display: flex; 6 | padding: 0 1rem 1rem 1rem; 7 | .sort-inputs { 8 | flex:1; 9 | } 10 | .filter-inputs { 11 | } 12 | .search-inputs { 13 | display: flex; 14 | .search-toggle { 15 | a { 16 | font-size: 1.5rem; 17 | color: $color-dark; 18 | line-height: 1.5; 19 | } 20 | } 21 | .search-field { 22 | display: block; 23 | position: relative; 24 | top:-5px; 25 | .search-hide { 26 | } 27 | input { 28 | display: inline-block; 29 | width:233px; 30 | margin:0 0 0 1rem; 31 | height:2rem; 32 | position: relative; 33 | top:2px; 34 | font-size: 1rem; 35 | } 36 | .search-submit { 37 | color: $color-primary; 38 | font-size: 1.5rem; 39 | line-height: 1.5; 40 | position: relative; 41 | top:5px; 42 | cursor: pointer; 43 | } 44 | } 45 | } 46 | label { 47 | display: inline-block; 48 | font-size: 1rem; 49 | margin:0 1rem 0 0; 50 | padding:0 0 0 0.5rem; 51 | color: $color-light; 52 | } 53 | select { 54 | width:auto; 55 | height:auto; 56 | margin:0; 57 | font-size: 1rem; 58 | padding: 0.3rem 1.875rem 0.3rem 0.2rem; 59 | border:0; 60 | color: $color-primary; 61 | max-width: 100px; 62 | &:focus { 63 | border:0; 64 | box-shadow: none; 65 | } 66 | } 67 | .totals { 68 | display: inline-block; 69 | color: $color-light; 70 | margin: 0; 71 | } 72 | } 73 | 74 | .search-message { 75 | text-align: center; 76 | a { 77 | font-weight: bold; 78 | } 79 | } 80 | 81 | 82 | .notebook-summary { 83 | padding:1rem; 84 | border-top:1px solid #e5e5e5; 85 | 86 | display: flex; 87 | flex-direction: row-reverse; 88 | flex-wrap: nowrap; 89 | justify-content: flex-end; 90 | align-items: center; 91 | 92 | &:nth-child(even) { 93 | 94 | } 95 | .specs { 96 | .title { 97 | font-size:1.4rem; 98 | margin:0 0 0.25rem 0; 99 | word-break: break-all; 100 | a { 101 | &:hover { 102 | } 103 | } 104 | } 105 | .date { 106 | margin:0 0 0.25rem 0; 107 | color: $color-light; 108 | a { 109 | } 110 | } 111 | .short { 112 | margin:0; 113 | //word-break: break-all; 114 | * { 115 | font-size:1rem; 116 | margin:0; 117 | padding:0; 118 | //display: inline; 119 | font-weight: 400; 120 | } 121 | a { 122 | word-break: break-all; 123 | } 124 | } 125 | } 126 | .avatar { 127 | margin:0 25px; 128 | flex-shrink: 0; 129 | height:50px; 130 | overflow: hidden; 131 | border-radius: 5px; 132 | border:1px solid $color-medium; 133 | img { 134 | width:50px; 135 | } 136 | } 137 | .stats { 138 | flex-shrink: 0; 139 | text-align: center; 140 | a { 141 | color: $color-dark; 142 | display: block; 143 | } 144 | ul { 145 | display: flex; 146 | list-style: none; 147 | padding:0; 148 | margin:0; 149 | } 150 | li { 151 | margin:0; 152 | padding:0; 153 | font-size: 0.9rem; 154 | width:60px; 155 | } 156 | .count { 157 | color: $color-primary; 158 | display: block; 159 | font-size: 1.5rem; 160 | } 161 | } 162 | 163 | .stats.display{ 164 | margin-top: 0px; 165 | color: $color-primary-text; 166 | font-size: 0.65em; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /client/src/containers/comment/CommentContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | 5 | import {editComment, submitReply, deleteComment} from '../../actions/auth/comment'; 6 | import {flagComment} from '../../actions/submission'; 7 | 8 | import Comment from '../../components/comments/Comment'; 9 | 10 | var actions = { 11 | editComment, 12 | deleteComment, 13 | flagComment, 14 | submitReply 15 | } 16 | 17 | class CommentContainer extends Component { 18 | constructor(props){ 19 | super(props); 20 | this.state = { 21 | comment: this.props.comment, 22 | replies: this.props.replies, 23 | author: this.props.author, 24 | authors: this.props.authors, 25 | currentUser: this.props.currentUser, 26 | error: this.props.error 27 | } 28 | } 29 | componentWillReceiveProps(nextProps) { 30 | if (this.props.error !== nextProps.error) { 31 | // updates the error / 32 | this.setState({ 33 | error: nextProps.error 34 | }) 35 | } 36 | if (nextProps.editedComment && nextProps.editedComment._id === nextProps.comment._id) { 37 | // updates the state of the comment if there is any editing / 38 | if ((!this.props.editedComment && nextProps.editedComment) || (nextProps.editedComment.content !== this.props.editedComment.content)) { 39 | this.setState({ 40 | comment: nextProps.editedComment 41 | }) 42 | } 43 | } 44 | if (nextProps.deletedComment && nextProps.deletedComment._id === nextProps.comment._id) { 45 | // updates the state of the comment if there is any deleting 46 | this.setState({ 47 | comment: nextProps.deletedComment 48 | }) 49 | } 50 | if (nextProps.replies && this.props.replies && (nextProps.replies.length !== this.props.replies.length)) { 51 | // updates the replies array state if there is any addition/ 52 | this.setState({ 53 | replies: nextProps.replies 54 | }) 55 | } 56 | } 57 | shouldComponentUpdate(nextProps, nextState) { 58 | // allows the component to render if there is any additional reply 59 | if (nextProps.replies && (nextProps.replies.length !== this.props.replies.length)) { 60 | return true; 61 | } 62 | // in case of editing comments, updates only the comment which has been edited 63 | if ((nextProps.editedComment || nextProps.error) && (nextProps.commentID !== nextState.comment._id)) { 64 | return false; 65 | } 66 | return true; 67 | } 68 | render(){ 69 | return( 70 | 71 | { this.state.comment.deleted ? '' : 72 |
    73 | 85 |
    86 | } 87 |
    88 | ) 89 | } 90 | } 91 | 92 | const mapStateToProps = (state, props) => { 93 | return { 94 | isAdmin: state.auth.isAdmin, 95 | editedComment: state.submissionByID.editedComment, 96 | deletedComment: state.submissionByID.deletedComment, 97 | error: state.submissionByID.error, 98 | commentID: state.submissionByID.commentID, 99 | } 100 | } 101 | 102 | const mapDispatchToProps = (dispatch) => { 103 | return { 104 | actions: bindActionCreators(actions, dispatch) 105 | } 106 | } 107 | 108 | export default connect(mapStateToProps, mapDispatchToProps)(CommentContainer); 109 | --------------------------------------------------------------------------------