├── .gitignore ├── .nvmrc ├── .prettierrc ├── .python-version ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── circle.yml ├── client ├── .babelrc ├── README.md ├── package-lock.json ├── package.json ├── project-logo.png ├── public │ ├── favicon.ico │ ├── index.html │ └── main.css └── src │ ├── agent.js │ ├── components │ ├── App.js │ ├── Article │ │ ├── ArticleActions.js │ │ ├── ArticleMeta.js │ │ ├── Comment.js │ │ ├── CommentContainer.js │ │ ├── CommentInput.js │ │ ├── CommentList.js │ │ ├── DeleteButton.js │ │ └── index.js │ ├── ArticleList.js │ ├── ArticlePreview.js │ ├── Editor.js │ ├── Header.js │ ├── Home │ │ ├── Banner.js │ │ ├── MainView.js │ │ ├── Tags.js │ │ └── index.js │ ├── ListErrors.js │ ├── ListPagination.js │ ├── Login.js │ ├── Profile.js │ ├── ProfileFavorites.js │ ├── Register.js │ └── Settings.js │ ├── constants │ └── actionTypes.js │ ├── index.js │ ├── middleware.js │ ├── reducer.js │ ├── reducers │ ├── article.js │ ├── articleList.js │ ├── auth.js │ ├── common.js │ ├── editor.js │ ├── home.js │ ├── profile.js │ └── settings.js │ └── store.js ├── cypress.json ├── cypress ├── README.md ├── fixtures │ ├── example.json │ └── post.js ├── integration │ ├── comments-spec.js │ ├── feeds-spec.js │ ├── follow-user-spec.js │ ├── force-logout-spec.js │ ├── login-spec.js │ ├── new-post-spec.js │ ├── pagination-spec.js │ ├── profile-spec.js │ ├── register-spec.js │ └── tags-spec.js ├── plugins │ └── index.js └── support │ └── index.js ├── images ├── app.png └── full-coverage.png ├── package-lock.json ├── package.json ├── renovate.json └── server ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── knexfile.js ├── lib ├── .hc.js ├── auth │ └── strategies │ │ └── jwt.js ├── bind.js ├── extensions │ └── error.js ├── index.js ├── migrations │ ├── 20180703003119_initial-user.js │ ├── 20180705235817_profile.js │ ├── 20180714221023_article-and-tag.js │ └── 20180722175030_article-comments.js ├── models │ ├── article.js │ ├── comment.js │ ├── helpers │ │ └── index.js │ ├── tag.js │ └── user.js ├── plugins │ ├── hapi-auth-jwt2.js │ ├── schmervice.js │ └── schwifty.js ├── routes │ ├── articles │ │ ├── create.js │ │ ├── delete.js │ │ ├── favorite.js │ │ ├── feed.js │ │ ├── fetch.js │ │ ├── list.js │ │ ├── unfavorite.js │ │ └── update.js │ ├── comments │ │ ├── by-article.js │ │ ├── create.js │ │ └── delete.js │ ├── helpers │ │ └── index.js │ ├── profiles │ │ ├── fetch.js │ │ ├── follow.js │ │ └── unfollow.js │ ├── tags │ │ └── fetch.js │ └── users │ │ ├── current.js │ │ ├── login.js │ │ ├── signup.js │ │ └── update.js └── services │ ├── article.js │ ├── display.js │ └── user.js ├── package-lock.json ├── package.json ├── server ├── .env-keep ├── index.js └── manifest.js └── test ├── index.js └── postman-collection.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # MacOS 64 | .DS_Store 65 | 66 | /cypress/videos/ 67 | /cypress/screenshots/ 68 | dist 69 | client/build 70 | client/.cache 71 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.13.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "arrowParens": "always" 7 | } -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 2.7.16 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorTheme": "Andromeda Italic Bordered", 3 | "workbench.colorCustomizations": {} 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Applitools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > 🚩 Looking for another real-world demonstration of Cypress in action? Check out the [Cypress Real World App repository](https://github.com/cypress-io/cypress-realworld-app). 2 | 3 | # Conduit App [![renovate-app badge][renovate-badge]][renovate-app] [![CircleCI](https://circleci.com/gh/cypress-io/cypress-example-conduit-app/tree/master.svg?style=svg&circle-token=f127e83138e505b26bb90ab7c0bcb60e5265fecb)](https://circleci.com/gh/cypress-io/cypress-example-conduit-app/tree/master) [![Coverage Status](https://coveralls.io/repos/github/cypress-io/cypress-example-conduit-app/badge.svg?branch=master)](https://coveralls.io/github/cypress-io/cypress-example-conduit-app?branch=master) [![Cypress.io Test Dashboard](https://img.shields.io/badge/cypress.io-dashboard-green.svg?style=flat-square)](https://dashboard.cypress.io/#/projects/bh5j1d) 4 | 5 | 6 | Fork of [applitools/cypress-applitools-webinar](https://github.com/applitools/cypress-applitools-webinar) which is a fork of [gothinkster/realworld](https://github.com/gothinkster/realworld) "Conduit" blogging application. 7 | 8 | ![Application](images/app.png) 9 | 10 | ## Tests 11 | 12 | The tests are in [cypress/integration](cypress/integration) folder 13 | 14 | - [feeds-spec.js](cypress/integration/feeds-spec.js) shows how to check the favorite articles feed and the global feed 15 | - [follow-user-spec.js](cypress/integration/follow-user-spec.js) shows how to create two users and check if one user can follow the other 16 | - [login-spec.js](cypress/integration/login-spec.js) checks if the user can log in via UI and via API 17 | - [new-post-spec.js](cypress/integration/new-post-spec.js) verifies that a new article can be published and updated 18 | - [profile-spec.js](cypress/integration/profile-spec.js) lets the user edit their profile 19 | - [register-spec.js](cypress/integration/register-spec.js) tests if a new user can register 20 | - [tags-spec.js](cypress/integration/tags-spec.js) checks if tags work 21 | - [pagination-spec.js](cypress/integration/pagination-spec.js) creates many articles via API calls and then checks if they are displayed across two pages 22 | - [force-logout-spec.js](cypress/integration/force-logout-spec.js) verifies that unauthorized API calls force the user session to finish 23 | 24 | ## Full code coverage 25 | 26 | Front- and back-end coverage for this application is collected using the [@cypress/code-coverage](https://github.com/cypress-io/code-coverage) plugin. You can run the locally instrumented server and client using `npm run dev:coverage` command. The backend coverage is exposed in [server/server/index.js](server/server/index.js) via endpoint listed in [cypress.json](cypress.json) (usually `GET /__coverage`). The frontend coverage is collected by instrumenting the web application source code on the fly, see the [client/.babelrc](client/.babelrc) file. 27 | 28 | The combined report is saved in `coverage/index.html` after the tests finish: 29 | 30 | ![Example full coverage report](images/full-coverage.png) 31 | 32 | The coverage is sent to [Coveralls.io](https://coveralls.io/repos/github/cypress-io/cypress-example-realworld) using command `npm run coveralls` from CircleCI AFTER partial coverage information from parallel E2E test runs is combined, see [circle.yml](circle.yml) file. 33 | 34 | ### Combining code coverage from parallel runs 35 | 36 | If you do not use an external code coverage service for combining code coverage reports, you need to combine those reports yourself like this repository is showing in [circle.yml](circle.yml) file. Several E2E `cypress/run` jobs run in parallel, each job saving its own coverage report folder. Then every job copies the report (using `save-partial-coverage-report` command) into a unique folder to avoid overwriting via reports from other machines. When all E2E jobs are finished, and reports are copied together, then the CI calls a command to merge the reports (see the `merge-coverage-reports` command that uses `nyc merge` tool). 37 | 38 | To learn more, read the [Cypress code coverage guide](https://on.cypress.io/coverage). 39 | 40 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 41 | [renovate-app]: https://renovateapp.com/ 42 | 43 | Requires Python 2.7 for node-gyp to be compiled. 44 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | # see orb options at 2 | # https://github.com/cypress-io/circleci-orb 3 | version: 2.1 4 | orbs: 5 | cypress: cypress-io/cypress@1 6 | 7 | executors: 8 | latest: 9 | docker: 10 | - image: cypress/browsers:node12.13.0-chrome78-ff70 11 | 12 | commands: 13 | save-partial-coverage-report: 14 | description: | 15 | Saves a single possibly partial coverage report by adding it to the 16 | workspace. This way different CircleCI jobs can run parts of the tests 17 | and save their results to be merged later. 18 | parameters: 19 | coverage-filename: 20 | type: string 21 | default: coverage/coverage-final.json 22 | description: | 23 | Path to the final coverage JSON file produced by "nyc" tool. 24 | Typically called "coverage/coverage-final.json" 25 | label: 26 | type: string 27 | default: default 28 | description: | 29 | Human name for the coverage file. For example, when saving both Cypress 30 | and Jest coverage file, use "cypress" and "jest" to have distinct filenames. 31 | steps: 32 | # do not crash if the coverage is not found 33 | # because a particular CI job might not have any tests to run 34 | # producing no coverage. 35 | - run: mkdir coverage-part || true 36 | - run: touch coverage-part/.placeholder || true 37 | # use unique job id to avoid accidentally overwriting coverage file 38 | # and in case the build is parallel, use node index too 39 | - run: cp <> coverage-part/coverage-<>-$CIRCLE_WORKFLOW_JOB_ID-index-$CIRCLE_NODE_INDEX.json || true 40 | - run: ls -la coverage-part 41 | - persist_to_workspace: 42 | root: ~/ 43 | paths: 44 | # note that the current folder is "project" 45 | # so we need to save the full path correctly 46 | # otherwise the files will not be restored in the expected location 47 | - 'project/coverage-part/*' 48 | 49 | merge-coverage-reports: 50 | description: | 51 | Merges individual code coverage files using "nyc" tool 52 | https://github.com/istanbuljs/nyc. 53 | All individual files should be in the folder "coverage-part" 54 | steps: 55 | - run: ls -la . 56 | - run: ls -la coverage-part || true 57 | - run: npx nyc merge coverage-part 58 | - run: mkdir .nyc_output || true 59 | # storing the combined report in ".nyc_output/out.json" 60 | # allows other NYC commands to find it right away 61 | - run: mv coverage.json .nyc_output/out.json 62 | - run: ls -la .nyc_output 63 | 64 | jobs: 65 | merge-coverage: 66 | description: Merges individual code coverage files and sends combined data to Coveralls.io 67 | executor: cypress/base-10 68 | steps: 69 | - attach_workspace: 70 | at: ~/ 71 | - merge-coverage-reports 72 | - run: 73 | name: generate coverage report 74 | command: | 75 | npx nyc report \ 76 | --reporter lcov --reporter text-summary \ 77 | --report-dir coverage 78 | - store_artifacts: 79 | path: coverage 80 | # send code coverage to coveralls.io 81 | # https://coveralls.io/github/cypress-io/cypress-example-realworld 82 | - run: 83 | command: npm run coveralls || true 84 | 85 | workflows: 86 | build: 87 | jobs: 88 | - cypress/install: 89 | executor: latest 90 | pre-steps: 91 | - run: npm config set unsafe-perm true 92 | 93 | - cypress/run: 94 | requires: 95 | - cypress/install 96 | executor: latest 97 | parallel: true 98 | parallelism: 2 99 | no-workspace: true 100 | start: npm run start:coverage 101 | wait-on: http://localhost:4100 102 | record: true 103 | post-steps: 104 | - store_artifacts: 105 | path: coverage 106 | # if this machine had no tests to run 107 | # there will be no coverage report 108 | - run: npx nyc report --reporter=text || true 109 | - save-partial-coverage-report: 110 | label: e2e 111 | 112 | - merge-coverage: 113 | requires: 114 | - cypress/run 115 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react"], 3 | "plugins": ["istanbul"] 4 | } 5 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # ![React + Redux Example App](project-logo.png) 2 | 3 | [![RealWorld Frontend](https://img.shields.io/badge/realworld-frontend-%23783578.svg)](http://realworld.io) 4 | 5 | > ### React + Redux codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld-example-apps) spec and API. 6 | 7 |    8 | 9 | ### [Demo](https://react-redux.realworld.io)    [RealWorld](https://github.com/gothinkster/realworld) 10 | 11 | Originally created for this [GH issue](https://github.com/reactjs/redux/issues/1353). The codebase is now feature complete; please submit bug fixes via pull requests & feedback via issues. 12 | 13 | We also have notes in [**our wiki**](https://github.com/gothinkster/react-redux-realworld-example-app/wiki) about how the various patterns used in this codebase and how they work (thanks [@thejmazz](https://github.com/thejmazz)!) 14 | 15 | 16 | ## Getting started 17 | 18 | You can view a live demo over at https://react-redux.realworld.io/ 19 | 20 | To get the frontend running locally: 21 | 22 | - Clone this repo 23 | - `npm install` to install all req'd dependencies 24 | - `npm start` to start the local server (this project uses create-react-app) 25 | 26 | Local web server will use port 4100 instead of standard React's port 3000 to prevent conflicts with some backends like Node or Rails. You can configure port in scripts section of `package.json`: we use [cross-env](https://github.com/kentcdodds/cross-env) to set environment variable PORT for React scripts, this is Windows-compatible way of setting environment variables. 27 | 28 | Alternatively, you can add `.env` file in the root folder of project to set environment variables (use PORT to change webserver's port). This file will be ignored by git, so it is suitable for API keys and other sensitive stuff. Refer to [dotenv](https://github.com/motdotla/dotenv) and [React](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-development-environment-variables-in-env) documentation for more details. Also, please remove setting variable via script section of `package.json` - `dotenv` never override variables if they are already set. 29 | 30 | ### Making requests to the backend API 31 | 32 | For convenience, we have a live API server running at https://conduit.productionready.io/api for the application to make requests against. You can view [the API spec here](https://github.com/GoThinkster/productionready/blob/master/api) which contains all routes & responses for the server. 33 | 34 | The source code for the backend server (available for Node, Rails and Django) can be found in the [main RealWorld repo](https://github.com/gothinkster/realworld). 35 | 36 | If you want to change the API URL to a local server, simply edit `src/agent.js` and change `API_ROOT` to the local server's URL (i.e. `http://localhost:3000/api`) 37 | 38 | 39 | ## Functionality overview 40 | 41 | The example application is a social blogging site (i.e. a Medium.com clone) called "Conduit". It uses a custom API for all requests, including authentication. You can view a live demo over at https://redux.productionready.io/ 42 | 43 | **General functionality:** 44 | 45 | - Authenticate users via JWT (login/signup pages + logout button on settings page) 46 | - CRU* users (sign up & settings page - no deleting required) 47 | - CRUD Articles 48 | - CR*D Comments on articles (no updating required) 49 | - GET and display paginated lists of articles 50 | - Favorite articles 51 | - Follow other users 52 | 53 | **The general page breakdown looks like this:** 54 | 55 | - Home page (URL: /#/ ) 56 | - List of tags 57 | - List of articles pulled from either Feed, Global, or by Tag 58 | - Pagination for list of articles 59 | - Sign in/Sign up pages (URL: /#/login, /#/register ) 60 | - Use JWT (store the token in localStorage) 61 | - Settings page (URL: /#/settings ) 62 | - Editor page to create/edit articles (URL: /#/editor, /#/editor/article-slug-here ) 63 | - Article page (URL: /#/article/article-slug-here ) 64 | - Delete article button (only shown to article's author) 65 | - Render markdown from server client side 66 | - Comments section at bottom of page 67 | - Delete comment button (only shown to comment's author) 68 | - Profile page (URL: /#/@username, /#/@username/favorites ) 69 | - Show basic user info 70 | - List of articles populated from author's created articles or author's favorited articles 71 | 72 |
73 | 74 | [![Brought to you by Thinkster](https://raw.githubusercontent.com/gothinkster/realworld/master/media/end.png)](https://thinkster.io) 75 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-realworld-example-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "cross-env": "5.2.1", 7 | "parcel-bundler": "1.12.4", 8 | "react-scripts": "3.4.1" 9 | }, 10 | "dependencies": { 11 | "@babel/core": "7.9.0", 12 | "@babel/preset-react": "7.9.4", 13 | "history": "4.10.1", 14 | "marked": "0.8.2", 15 | "prop-types": "15.7.2", 16 | "react": "16.13.1", 17 | "react-dom": "16.13.1", 18 | "react-redux": "5.1.2", 19 | "react-router": "4.3.1", 20 | "react-router-dom": "4.3.1", 21 | "react-router-redux": "5.0.0-alpha.8", 22 | "redux": "4.0.5", 23 | "redux-devtools-extension": "2.13.8", 24 | "redux-logger": "3.0.6", 25 | "superagent": "5.2.2", 26 | "superagent-promise": "1.1.0" 27 | }, 28 | "scripts": { 29 | "start-old": "cross-env PORT=4100 BROWSER=NONE react-scripts start", 30 | "start": "parcel serve --port 4100 public/index.html", 31 | "build": "cross-env CI= react-scripts build", 32 | "test": "cross-env PORT=4100 react-scripts test --env=jsdom", 33 | "eject": "react-scripts eject" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/project-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-conduit-app/c94a137c0f18cb9d5f930ac2f272c11c55c98998/client/project-logo.png -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-conduit-app/c94a137c0f18cb9d5f930ac2f272c11c55c98998/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 18 | 27 | Conduit 28 | 29 | 30 |
31 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/src/agent.js: -------------------------------------------------------------------------------- 1 | import superagentPromise from 'superagent-promise'; 2 | import _superagent from 'superagent'; 3 | 4 | const superagent = superagentPromise(_superagent, global.Promise); 5 | 6 | const API_ROOT = 'http://localhost:3000/api'; 7 | 8 | const encode = encodeURIComponent; 9 | const responseBody = res => res.body; 10 | 11 | let token = null; 12 | const tokenPlugin = req => { 13 | if (token) { 14 | req.set('authorization', `Token ${token}`); 15 | } 16 | } 17 | 18 | const requests = { 19 | del: url => 20 | superagent.del(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody), 21 | get: url => 22 | superagent.get(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody), 23 | put: (url, body) => 24 | superagent.put(`${API_ROOT}${url}`, body).use(tokenPlugin).then(responseBody), 25 | post: (url, body) => 26 | superagent.post(`${API_ROOT}${url}`, body).use(tokenPlugin).then(responseBody) 27 | }; 28 | 29 | const Auth = { 30 | current: () => 31 | requests.get('/user'), 32 | login: (email, password) => 33 | requests.post('/users/login', { user: { email, password } }), 34 | register: (username, email, password) => 35 | requests.post('/users', { user: { username, email, password } }), 36 | save: user => 37 | requests.put('/user', { user }) 38 | }; 39 | 40 | const Tags = { 41 | getAll: () => requests.get('/tags') 42 | }; 43 | 44 | const limit = (count, p) => `limit=${count}&offset=${p ? p * count : 0}`; 45 | const omitSlug = article => Object.assign({}, article, { slug: undefined }) 46 | const Articles = { 47 | all: page => 48 | requests.get(`/articles?${limit(10, page)}`), 49 | byAuthor: (author, page) => 50 | requests.get(`/articles?author=${encode(author)}&${limit(5, page)}`), 51 | byTag: (tag, page) => 52 | requests.get(`/articles?tag=${encode(tag)}&${limit(10, page)}`), 53 | del: slug => 54 | requests.del(`/articles/${slug}`), 55 | favorite: slug => 56 | requests.post(`/articles/${slug}/favorite`), 57 | favoritedBy: (author, page) => 58 | requests.get(`/articles?favorited=${encode(author)}&${limit(5, page)}`), 59 | feed: () => 60 | requests.get('/articles/feed?limit=10&offset=0'), 61 | get: slug => 62 | requests.get(`/articles/${slug}`), 63 | unfavorite: slug => 64 | requests.del(`/articles/${slug}/favorite`), 65 | update: article => 66 | requests.put(`/articles/${article.slug}`, { article: omitSlug(article) }), 67 | create: article => 68 | requests.post('/articles', { article }) 69 | }; 70 | 71 | const Comments = { 72 | create: (slug, comment) => 73 | requests.post(`/articles/${slug}/comments`, { comment }), 74 | delete: (slug, commentId) => 75 | requests.del(`/articles/${slug}/comments/${commentId}`), 76 | forArticle: slug => 77 | requests.get(`/articles/${slug}/comments`) 78 | }; 79 | 80 | const Profile = { 81 | follow: username => 82 | requests.post(`/profiles/${username}/follow`), 83 | get: username => 84 | requests.get(`/profiles/${username}`), 85 | unfollow: username => 86 | requests.del(`/profiles/${username}/follow`) 87 | }; 88 | 89 | export default { 90 | Articles, 91 | Auth, 92 | Comments, 93 | Profile, 94 | Tags, 95 | setToken: _token => { token = _token; } 96 | }; 97 | -------------------------------------------------------------------------------- /client/src/components/App.js: -------------------------------------------------------------------------------- 1 | import agent from '../agent'; 2 | import Header from './Header'; 3 | import React from 'react'; 4 | import { connect } from 'react-redux'; 5 | import { APP_LOAD, REDIRECT } from '../constants/actionTypes'; 6 | import { Route, Switch } from 'react-router-dom'; 7 | import Article from '../components/Article'; 8 | import Editor from '../components/Editor'; 9 | import Home from '../components/Home'; 10 | import Login from '../components/Login'; 11 | import Profile from '../components/Profile'; 12 | import ProfileFavorites from '../components/ProfileFavorites'; 13 | import Register from '../components/Register'; 14 | import Settings from '../components/Settings'; 15 | import { store } from '../store'; 16 | import { push } from 'react-router-redux'; 17 | 18 | const mapStateToProps = state => { 19 | return { 20 | appLoaded: state.common.appLoaded, 21 | appName: state.common.appName, 22 | currentUser: state.common.currentUser, 23 | redirectTo: state.common.redirectTo 24 | }}; 25 | 26 | const mapDispatchToProps = dispatch => ({ 27 | onLoad: (payload, token) => 28 | dispatch({ type: APP_LOAD, payload, token, skipTracking: true }), 29 | onRedirect: () => 30 | dispatch({ type: REDIRECT }) 31 | }); 32 | 33 | class App extends React.Component { 34 | componentWillReceiveProps(nextProps) { 35 | if (nextProps.redirectTo) { 36 | // this.context.router.replace(nextProps.redirectTo); 37 | store.dispatch(push(nextProps.redirectTo)); 38 | this.props.onRedirect(); 39 | } 40 | } 41 | 42 | componentWillMount() { 43 | const token = window.localStorage.getItem('jwt'); 44 | if (token) { 45 | agent.setToken(token); 46 | } 47 | 48 | this.props.onLoad(token ? agent.Auth.current() : null, token); 49 | } 50 | 51 | render() { 52 | if (this.props.appLoaded) { 53 | return ( 54 |
55 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 | ); 71 | } 72 | return ( 73 |
74 |
77 |
78 | ); 79 | } 80 | } 81 | 82 | // App.contextTypes = { 83 | // router: PropTypes.object.isRequired 84 | // }; 85 | 86 | export default connect(mapStateToProps, mapDispatchToProps)(App); 87 | -------------------------------------------------------------------------------- /client/src/components/Article/ArticleActions.js: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom' 2 | import React from 'react' 3 | import agent from '../../agent' 4 | import { connect } from 'react-redux' 5 | import { DELETE_ARTICLE } from '../../constants/actionTypes' 6 | 7 | const mapDispatchToProps = dispatch => ({ 8 | onClickDelete: payload => dispatch({ type: DELETE_ARTICLE, payload }) 9 | }) 10 | 11 | const ArticleActions = props => { 12 | const article = props.article 13 | const del = () => { 14 | props.onClickDelete(agent.Articles.del(article.slug)) 15 | } 16 | if (props.canModify) { 17 | return ( 18 | 19 | 24 | Edit Article 25 | 26 | 27 | 34 | 35 | ) 36 | } 37 | 38 | return 39 | } 40 | 41 | export default connect( 42 | () => ({}), 43 | mapDispatchToProps 44 | )(ArticleActions) 45 | -------------------------------------------------------------------------------- /client/src/components/Article/ArticleMeta.js: -------------------------------------------------------------------------------- 1 | import ArticleActions from './ArticleActions'; 2 | import { Link } from 'react-router-dom'; 3 | import React from 'react'; 4 | 5 | const ArticleMeta = props => { 6 | const article = props.article; 7 | return ( 8 |
9 | 10 | {article.author.username} 11 | 12 | 13 |
14 | 15 | {article.author.username} 16 | 17 | 18 | {new Date(article.createdAt).toDateString()} 19 | 20 |
21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default ArticleMeta; 28 | -------------------------------------------------------------------------------- /client/src/components/Article/Comment.js: -------------------------------------------------------------------------------- 1 | import DeleteButton from './DeleteButton' 2 | import { Link } from 'react-router-dom' 3 | import React from 'react' 4 | 5 | const Comment = props => { 6 | const comment = props.comment 7 | const show = 8 | props.currentUser && props.currentUser.username === comment.author.username 9 | return ( 10 |
11 |
12 |

{comment.body}

13 |
14 |
15 | 16 | {comment.author.username} 21 | 22 |   23 | 24 | {comment.author.username} 25 | 26 | 27 | {new Date(comment.createdAt).toDateString()} 28 | 29 | 30 |
31 |
32 | ) 33 | } 34 | 35 | export default Comment 36 | -------------------------------------------------------------------------------- /client/src/components/Article/CommentContainer.js: -------------------------------------------------------------------------------- 1 | import CommentInput from './CommentInput'; 2 | import CommentList from './CommentList'; 3 | import { Link } from 'react-router-dom'; 4 | import React from 'react'; 5 | 6 | const CommentContainer = props => { 7 | if (props.currentUser) { 8 | return ( 9 |
10 |
11 | 12 | 13 |
14 | 15 | 19 |
20 | ); 21 | } else { 22 | return ( 23 |
24 |

25 | Sign in 26 |  or  27 | sign up 28 |  to add comments on this article. 29 |

30 | 31 | 35 |
36 | ); 37 | } 38 | }; 39 | 40 | export default CommentContainer; 41 | -------------------------------------------------------------------------------- /client/src/components/Article/CommentInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import agent from '../../agent' 3 | import { connect } from 'react-redux' 4 | import { ADD_COMMENT } from '../../constants/actionTypes' 5 | 6 | const mapDispatchToProps = dispatch => ({ 7 | onSubmit: payload => dispatch({ type: ADD_COMMENT, payload }) 8 | }) 9 | 10 | class CommentInput extends React.Component { 11 | constructor () { 12 | super() 13 | this.state = { 14 | body: '' 15 | } 16 | 17 | this.setBody = ev => { 18 | this.setState({ body: ev.target.value }) 19 | } 20 | 21 | this.createComment = ev => { 22 | ev.preventDefault() 23 | const payload = agent.Comments.create(this.props.slug, { 24 | body: this.state.body 25 | }) 26 | this.setState({ body: '' }) 27 | this.props.onSubmit(payload) 28 | } 29 | } 30 | 31 | render () { 32 | return ( 33 |
34 |
35 |