├── .babelrc ├── .browserslistrc ├── .dependabot └── config.yml ├── .editorconfig ├── .env.development ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .graphqlconfig ├── .huskyrc ├── .lintstagedrc ├── .mergify.yml ├── .prettierrc.js ├── .stylelintrc ├── Dockerfile ├── Procfile ├── README.md ├── __mocks__ └── styleMock.ts ├── apollo.config.js ├── app.json ├── commitlint.config.js ├── docker-compose.yml ├── jest.config.js ├── node-type-orm.graphql ├── nodemon.json ├── package.json ├── server ├── .babelrc ├── .eslintrc.js ├── config.ts ├── dev.ts ├── middleware │ ├── dev.ts │ └── prod.ts └── prod.ts ├── src ├── apolloClient.ts ├── app.tsx ├── appHistory.ts ├── components │ ├── Button │ │ └── index.tsx │ ├── Content │ │ └── index.ts │ ├── Empty │ │ └── index.tsx │ ├── ErrorAlert │ │ ├── index.tsx │ │ └── styled.ts │ ├── FormButton │ │ └── index.ts │ ├── FormElementDescription │ │ └── index.ts │ ├── FormElementError │ │ └── index.ts │ ├── FormElementLabel │ │ └── index.ts │ ├── FormElementLink │ │ └── index.ts │ ├── Layout │ │ └── index.ts │ ├── Loader │ │ └── index.tsx │ ├── Navigation │ │ ├── index.tsx │ │ └── styled.ts │ ├── TextInput │ │ ├── index.tsx │ │ └── styled.ts │ ├── Textarea │ │ ├── index.tsx │ │ └── styled.ts │ └── Typography │ │ ├── H1.tsx │ │ └── H3.tsx ├── config.ts ├── globalTypes.ts ├── index.html ├── index.tsx ├── modules │ ├── auth │ │ ├── forms │ │ │ ├── ChangePassword │ │ │ │ └── index.tsx │ │ │ ├── ForgotPassword │ │ │ │ └── index.tsx │ │ │ ├── Login │ │ │ │ └── index.tsx │ │ │ └── Register │ │ │ │ └── index.tsx │ │ ├── gql │ │ │ ├── __generated__ │ │ │ │ ├── AccessToken.ts │ │ │ │ ├── ChangePassword.ts │ │ │ │ ├── ForgotPassword.ts │ │ │ │ ├── Login.ts │ │ │ │ ├── Me.ts │ │ │ │ └── Register.ts │ │ │ └── index.ts │ │ └── hooks │ │ │ ├── useChangePassword.ts │ │ │ ├── useForgotPassword.ts │ │ │ ├── useLogin.ts │ │ │ ├── useMe.ts │ │ │ └── useRegister.ts │ ├── blog │ │ ├── cache │ │ │ └── updateListPages.ts │ │ ├── components │ │ │ ├── PageCard │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── PagesList │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ └── RelevantPagesList │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ ├── forms │ │ │ └── Page │ │ │ │ └── index.tsx │ │ ├── gql │ │ │ ├── __generated__ │ │ │ │ ├── CreatePage.ts │ │ │ │ ├── DeletePage.ts │ │ │ │ ├── ListPages.ts │ │ │ │ ├── PageDetail.ts │ │ │ │ └── UpdatePage.ts │ │ │ └── index.ts │ │ └── hooks │ │ │ ├── useCreatePage.ts │ │ │ ├── useDeletePage.ts │ │ │ ├── usePageDetail.ts │ │ │ └── useUpdatePage.ts │ └── router │ │ ├── previousLocation.ts │ │ └── routes │ │ └── Protected.tsx ├── pages │ ├── Auth │ │ ├── Login │ │ │ ├── index.tsx │ │ │ └── test │ │ │ │ └── Login.test.tsx │ │ ├── Logout │ │ │ └── index.ts │ │ ├── Me │ │ │ ├── index.tsx │ │ │ ├── styled.ts │ │ │ └── test │ │ │ │ └── Me.test.tsx │ │ ├── PasswordForgot │ │ │ ├── index.tsx │ │ │ └── test │ │ │ │ └── PasswordForgot.test.tsx │ │ ├── PasswordReset │ │ │ ├── index.tsx │ │ │ └── test │ │ │ │ └── PasswordReset.test.tsx │ │ └── Register │ │ │ ├── index.tsx │ │ │ └── test │ │ │ └── Register.test.tsx │ ├── Blog │ │ ├── Create │ │ │ └── index.tsx │ │ ├── Detail │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ └── Edit │ │ │ ├── index.tsx │ │ │ └── test │ │ │ └── Edit.test.tsx │ ├── Home │ │ ├── index.tsx │ │ └── test │ │ │ └── Home.test.tsx │ └── NotFound │ │ ├── index.tsx │ │ └── test │ │ └── NotFound.test.tsx ├── routes.tsx ├── services │ ├── auth │ │ └── index.ts │ ├── messages │ │ └── index.ts │ └── token │ │ └── index.ts ├── styles │ ├── index.ts │ └── mediaQueries.ts ├── test-utils │ ├── ApolloProvider.tsx │ ├── form │ │ └── input.ts │ ├── generators │ │ └── index.ts │ ├── gql │ │ └── index.ts │ ├── render.tsx │ └── setup.ts └── types.d.ts ├── tsconfig.json ├── webpack ├── client │ ├── common.js │ ├── dev.js │ └── prod.js ├── config.js └── server │ └── prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/react", 4 | "@babel/typescript", 5 | [ 6 | "@babel/env", 7 | { 8 | "modules": false 9 | } 10 | ] 11 | ], 12 | "plugins": ["@babel/plugin-transform-runtime"] 13 | } 14 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | [production] 2 | >0.2% 3 | not dead 4 | not op_mini all 5 | 6 | [development] 7 | last 1 chrome version 8 | last 1 firefox version 9 | last 1 safari version 10 | -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | 4 | update_configs: 5 | - package_manager: "javascript" 6 | directory: "/" 7 | update_schedule: "daily" 8 | version_requirement_updates: increase_versions 9 | commit_message: 10 | prefix: "chore" 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs. 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # We recommend you to keep these unchanged. 10 | charset = utf-8 11 | end_of_line = lf 12 | indent_size = 2 13 | indent_style = space 14 | insert_final_newline = true 15 | trim_trailing_whitespace = true 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | SERVER_URL=https://node-type-orm-graphql.herokuapp.com/graphql 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /webpack 2 | /node_modules 3 | /build 4 | /coverage 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@code-quality/eslint-config-react', 4 | '@code-quality/eslint-config-typescript', 5 | 'prettier', 6 | 'prettier/react' 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: 12 13 | registry-url: https://registry.npmjs.org 14 | - name: Install Dependencies 15 | run: yarn install 16 | - name: Lint TS Files 17 | run: yarn lint:ts 18 | - name: Lint CSS Files 19 | run: yarn lint:css 20 | - name: Run type check 21 | run: yarn type-check 22 | - name: Test & publish code coverage 23 | uses: paambaati/codeclimate-action@v2.3.0 24 | env: 25 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 26 | with: 27 | coverageCommand: yarn test:coverage 28 | debug: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /build 3 | /coverage 4 | /node_modules 5 | /dist 6 | -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node Type ORM GraphQL Schema", 3 | "schemaPath": "node-type-orm.graphql", 4 | "extensions": { 5 | "endpoints": { 6 | "Default Endpoint": { 7 | "url": "https://node-type-orm-graphql.herokuapp.com/graphql", 8 | "introspect": true 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 4 | "pre-commit": "lint-staged" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,tsx}": [ 3 | "eslint --fix", 4 | "prettier --write", 5 | "stylelint", 6 | "git add" 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge for Dependabot pull requests 3 | conditions: 4 | - author=dependabot-preview[bot] 5 | actions: 6 | merge: 7 | method: merge 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@code-quality/prettier-config') 2 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@code-quality/stylelint-styled-components-config", 4 | "stylelint-config-prettier" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | ENV NODE_ENV='production' 4 | ENV SERVER_URL='https://node-type-orm-graphql.herokuapp.com/graphql' 5 | 6 | # Create app directory 7 | RUN mkdir -p /usr/src/app 8 | WORKDIR /usr/src/app 9 | 10 | # Install app dependencies 11 | COPY package.json /usr/src/app/ 12 | COPY yarn.lock /usr/src/app/ 13 | RUN yarn install 14 | 15 | # Bundle app source 16 | COPY . /usr/src/app 17 | RUN yarn build 18 | EXPOSE 3000 19 | CMD [ "yarn", "prod" ] 20 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run prod 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/developer239/react-apollo-graphql/workflows/CI/badge.svg)](https://github.com/developer239/react-apollo-graphql/actions?query=workflow%3A%22CI%22) 2 | [![Test Coverage](https://api.codeclimate.com/v1/badges/8b605e0fb1af6dc86063/test_coverage)](https://codeclimate.com/github/developer239/react-apollo-graphql/test_coverage) 3 | [![Maintainability](https://api.codeclimate.com/v1/badges/8b605e0fb1af6dc86063/maintainability)](https://codeclimate.com/github/developer239/react-apollo-graphql/maintainability) 4 | [![Dependabot](https://badgen.net/dependabot/developer239/react-apollo-graphql/84358471?icon=dependabot)](https://dependabot.com/) 5 | [![Mergify Status](https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/developer239/react-apollo-graphql&style=flat)](https://mergify.io) 6 | 7 | ## React Apollo GraphQL [from scratch] 8 | 9 | 10 | Today you have basically two ways how to start new React application: [NextJs](https://github.com/zeit/next.js/) or [Create React App](https://github.com/facebook/create-react-app). 11 | 12 | However, there is no fun in using other peoples frameworks so I created this application from scratch. If you ever wondered how to set up your own boilerplate. This is a good place to start. 13 | 14 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 15 | 16 | ## Demo 17 | 18 | You can try the application [here](https://react-apollo-graphql.herokuapp.com) (it might take a while before the free server wakes up) 19 | 20 | 📘 Backend API is running [here](https://node-type-orm-graphql.herokuapp.com/graphql). You can find the source code of the backend application [here](https://github.com/developer239/node-type-orm-graphql). 21 | 22 | # Development 23 | 24 | System Dependencies: 25 | 26 | 1. `brew install node` 27 | 2. `brew install yarn` 28 | 29 | Run development server: 30 | 31 | 1. `yarn install` 32 | 2. `yarn apollo:generate-types:watch` 33 | 3. `yarn watch` 34 | 35 | ## Useful Commands 36 | 37 | - `yarn lint:ts` lint TS files 38 | - `yarn lint:css` lint CSS 39 | - `yarn lint:circular-dependencies` detect circular dependencies 40 | - `yarn apollo:generate-types` generate TS definitions from GraphQL schema 41 | - `yarn apollo:remove-all-types` remove all automatically generated TS definitions 42 | - `yarn test` run jest 43 | - `docker-compose up` run the application in Docker 🐳 container 44 | 45 | ## TODO 46 | 47 | - [ ] Optimize [antd](https://ant.design/docs/react/introduce) package with [babel-plugin-import](https://www.npmjs.com/package/babel-plugin-import) 48 | - [ ] Create custom vendors config with [DllPlugin](https://webpack.js.org/plugins/dll-plugin/) 49 | - [x] Implement _request password reset_ + _request password change_ 50 | - [x] Implement _automatic token refresh_ 51 | 52 | # Production 53 | 54 | Keep in mind that `main` and `vendors` packages **are huge**. I plan to implement [babel-plugin-import](https://www.npmjs.com/package/babel-plugin-import) and [DllPlugin](https://webpack.js.org/plugins/dll-plugin/) in the future. 55 | 56 | 1. `SERVER_URL=https://node-type-orm-graphql.herokuapp.com/graphql yarn build` 57 | 2. `yarn prod` 58 | -------------------------------------------------------------------------------- /__mocks__/styleMock.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | export default {} 3 | -------------------------------------------------------------------------------- /apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | service: { 4 | name: 'node-type-orm-graphql', 5 | localSchemaFile: 'node-type-orm.graphql', 6 | }, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Apollo GraphQl", 3 | "description": "React in tandem with Apollo. Minimal implementation that will help you get started with GraphQL.", 4 | "repository": "https://github.com/developer239/react-apollo-graphql", 5 | "keywords": [ 6 | "node", 7 | "apollo", 8 | "react", 9 | "react-apollo", 10 | "graphql", 11 | "typescript", 12 | "react-router", 13 | "express", 14 | "formik", 15 | "yup" 16 | ], 17 | "website": "https://react-apollo-graphql.herokuapp.com", 18 | "env": { 19 | "SERVER_URL": { 20 | "description": "This is where our GraphQL backend lives.", 21 | "value": "https://node-type-orm-graphql.herokuapp.com/graphql" 22 | }, 23 | "NODE_ENV": { 24 | "description": "We want to run the app in production mode.", 25 | "value": "production" 26 | }, 27 | "YARN_PRODUCTION": { 28 | "description": "We want to keep dev dependencies.", 29 | "value": "false" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@code-quality/commitlint-config', 4 | ], 5 | } 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | web: 4 | container_name: react_apollo_graphql 5 | build: . 6 | ports: 7 | - "3000:3000" 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | modulePaths: ['src'], 5 | setupFilesAfterEnv: ['/src/test-utils/setup.ts'], 6 | moduleFileExtensions: ['ts', 'tsx', 'js'], 7 | verbose: true, 8 | coverageReporters: ['json', 'lcov', 'text'], 9 | collectCoverageFrom: [ 10 | 'src/**/*.{ts,tsx}', 11 | ], 12 | moduleNameMapper: { 13 | '\\.(css|less)$': '/__mocks__/styleMock.ts', 14 | }, 15 | globals: { 16 | 'ts-jest': { 17 | 'diagnostics': false, 18 | }, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /node-type-orm.graphql: -------------------------------------------------------------------------------- 1 | # This file was generated based on ".graphqlconfig". Do not edit manually. 2 | 3 | schema { 4 | query: Query 5 | mutation: Mutation 6 | } 7 | 8 | type Mutation { 9 | changePassword(data: ChangePasswordInput!): Session 10 | createPage(data: CreatePageInput!): Page! 11 | deletePage(id: Float!): Page! 12 | forgotPassword(email: String!): Boolean! 13 | login(email: String!, password: String!): Session! 14 | register(data: RegisterInput!): Session! 15 | updatePage(data: UpdatePageInput!): Page! 16 | } 17 | 18 | type Page { 19 | id: ID! 20 | text: String! 21 | title: String! 22 | uri: String! 23 | user: User! 24 | } 25 | 26 | type Query { 27 | accessToken(refreshToken: String!): String! 28 | listPages: [Page!]! 29 | me: User 30 | pageDetail(id: Float!): Page! 31 | } 32 | 33 | type RefreshToken { 34 | id: ID! 35 | token: String! 36 | } 37 | 38 | type ResetPasswordToken { 39 | expires: DateTime! 40 | id: ID! 41 | token: String! 42 | } 43 | 44 | type Session { 45 | accessToken: String! 46 | refreshToken: String! 47 | user: User! 48 | } 49 | 50 | type User { 51 | email: String! 52 | firstName: String! 53 | id: ID! 54 | lastName: String! 55 | pages: [Page!] 56 | } 57 | 58 | input ChangePasswordInput { 59 | password: String! 60 | token: String! 61 | } 62 | 63 | input CreatePageInput { 64 | text: String! 65 | title: String! 66 | } 67 | 68 | input RegisterInput { 69 | email: String! 70 | firstName: String! 71 | lastName: String! 72 | password: String! 73 | } 74 | 75 | input UpdatePageInput { 76 | id: Float! 77 | text: String! 78 | title: String! 79 | } 80 | 81 | 82 | "The javascript `Date` as string. Type represents date and time as the ISO Date string." 83 | scalar DateTime 84 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "node_modules" 4 | ], 5 | "watch": [ 6 | "server", 7 | "webpack", 8 | "yarn.lock" 9 | ], 10 | "exec": "babel-node --extensions '.ts' ./server/dev.ts", 11 | "ext": "ts" 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-apollo-graphql", 3 | "version": "5.0.0", 4 | "description": "JavaScript meets web. React in tandem with Apollo. Minimal implementation that will help you get started with GraphQL.", 5 | "repository": "https://github.com/developer239/react-apollo-graphql.git", 6 | "author": "Michal Jarnot ", 7 | "license": "MIT", 8 | "scripts": { 9 | "watch": "nodemon", 10 | "build:client": "webpack --config ./webpack/client/prod.js", 11 | "build:server": "webpack --config ./webpack/server/prod.js", 12 | "build": "npm run build:client && npm run build:server", 13 | "prod": "node build/server.js", 14 | "format": "prettier --write '*/**/*.{ts,tsx,css,md,json}'", 15 | "lint:ts": "eslint '{server,src}/**/*.{ts,tsx}'", 16 | "lint:css": "stylelint 'src/**/*.{ts,tsx}'", 17 | "lint:circular-dependencies": "madge --circular --extensions ts,tsx ./src", 18 | "apollo:generate-types": " apollo client:codegen --target=typescript --globalTypesFile src/globalTypes.ts", 19 | "apollo:generate-types:watch": "npm run apollo:generate-types -- --watch", 20 | "apollo:remove-all-types": "find . -regex './src/.*__generated__.*' -type d -prune -exec rm -r \"{}\" \\;", 21 | "test": "jest", 22 | "test:coverage": "jest --coverage", 23 | "heroku-postbuild": "npm run build", 24 | "type-check": "tsc" 25 | }, 26 | "dependencies": { 27 | "@apollo/react-hooks": "3.1.3", 28 | "antd": "3.26.8", 29 | "apollo-cache": "1.3.4", 30 | "apollo-cache-inmemory": "1.6.5", 31 | "apollo-client": "2.6.8", 32 | "apollo-link": "1.2.13", 33 | "apollo-link-context": "1.0.19", 34 | "apollo-link-error": "^1.1.12", 35 | "apollo-link-http": "1.5.16", 36 | "compression": "1.7.4", 37 | "express": "4.17.1", 38 | "formik": "2.1.4", 39 | "graphql": "14.6.0", 40 | "graphql-tag": "2.10.3", 41 | "history": "4.10.1", 42 | "invariant": "2.2.4", 43 | "js-cookie": "2.2.1", 44 | "react": "16.12.0", 45 | "react-dom": "16.12.0", 46 | "react-lines-ellipsis": "0.14.1", 47 | "react-nl2br": "0.6.0", 48 | "react-router": "5.1.2", 49 | "react-router-dom": "5.1.2", 50 | "styled-components": "4.4.1", 51 | "yup": "0.28.1" 52 | }, 53 | "devDependencies": { 54 | "@apollo/react-testing": "3.1.3", 55 | "@babel/core": "7.8.4", 56 | "@babel/node": "7.8.4", 57 | "@babel/plugin-transform-runtime": "7.8.3", 58 | "@babel/preset-env": "7.8.4", 59 | "@babel/preset-react": "7.8.3", 60 | "@babel/preset-typescript": "7.8.3", 61 | "@code-quality/commitlint-config": "^1.1.0", 62 | "@code-quality/eslint-config-node": "^1.6.0", 63 | "@code-quality/eslint-config-react": "^1.7.0", 64 | "@code-quality/eslint-config-typescript": "^1.6.0", 65 | "@code-quality/prettier-config": "2.1.0", 66 | "@code-quality/stylelint-styled-components-config": "^1.1.0", 67 | "@commitlint/cli": "^8.3.5", 68 | "@testing-library/react": "9.4.0", 69 | "@types/clean-webpack-plugin": "0.1.3", 70 | "@types/compression": "1.0.1", 71 | "@types/express": "4.17.2", 72 | "@types/faker": "4.1.9", 73 | "@types/graphql": "14.5.0", 74 | "@types/history": "4.7.5", 75 | "@types/invariant": "2.2.31", 76 | "@types/jest": "25.1.2", 77 | "@types/js-cookie": "2.2.4", 78 | "@types/react": "16.9.19", 79 | "@types/react-dom": "16.9.5", 80 | "@types/react-router": "5.1.4", 81 | "@types/react-router-dom": "5.1.3", 82 | "@types/styled-components": "4.4.2", 83 | "@types/webpack-dev-middleware": "2.0.3", 84 | "@types/webpack-env": "1.15.1", 85 | "@types/webpack-hot-middleware": "2.25.0", 86 | "@types/webpack-merge": "4.1.5", 87 | "@types/yup": "0.26.30", 88 | "apollo": "2.22.0", 89 | "babel-jest": "25.1.0", 90 | "babel-loader": "8.0.6", 91 | "clean-webpack-plugin": "3.0.0", 92 | "css-loader": "3.4.2", 93 | "dotenv-webpack": "1.7.0", 94 | "eslint": "^6.8.0", 95 | "eslint-config-prettier": "^6.10.0", 96 | "eslint-import-resolver-webpack": "0.12.1", 97 | "eslint-plugin-import": "^2.20.1", 98 | "faker": "4.1.0", 99 | "html-loader": "0.5.5", 100 | "html-webpack-plugin": "3.2.0", 101 | "husky": "4.2.1", 102 | "jest": "25.1.0", 103 | "lint-staged": "10.0.7", 104 | "madge": "3.7.0", 105 | "nodemon": "2.0.2", 106 | "prettier": "1.19.1", 107 | "source-map-loader": "0.2.4", 108 | "style-loader": "1.1.3", 109 | "stylelint": "13.0.0", 110 | "stylelint-config-prettier": "^8.0.1", 111 | "ts-jest": "25.2.0", 112 | "typescript": "3.7.5", 113 | "webpack": "4.41.5", 114 | "webpack-cli": "3.3.10", 115 | "webpack-dev-middleware": "3.7.2", 116 | "webpack-dev-server": "3.10.3", 117 | "webpack-hot-middleware": "2.25.0", 118 | "webpack-merge": "4.2.2" 119 | }, 120 | "keywords": [ 121 | "node", 122 | "apollo", 123 | "react", 124 | "react-apollo", 125 | "graphql", 126 | "typescript", 127 | "react-router", 128 | "express", 129 | "formik", 130 | "yup" 131 | ] 132 | } 133 | -------------------------------------------------------------------------------- /server/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/typescript", 4 | "@babel/env" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@code-quality/eslint-config-node', 4 | '@code-quality/eslint-config-typescript', 5 | ], 6 | settings: { 7 | 'import/resolver': { 8 | node: { 9 | paths: [ 10 | 'server', 11 | ], 12 | }, 13 | }, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /server/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export const PORT = process.env.PORT || 3000 4 | 5 | export const BUILD_DIR_PUBLIC = process.env.BUILD_DIR_PUBLIC || 'public' 6 | 7 | export const PATH_TO_BUILD_DIR_PUBLIC = 8 | process.env.PATH_TO_BUILD_DIR_PUBLIC || 9 | path.resolve(__dirname, BUILD_DIR_PUBLIC) 10 | -------------------------------------------------------------------------------- /server/dev.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import express from 'express' 3 | import { PATH_TO_BUILD_DIR_PUBLIC, PORT } from './config' 4 | import { 5 | handleServeBaseRouteDev, 6 | handleWebpackDevServer, 7 | } from './middleware/dev' 8 | 9 | const app = express() 10 | 11 | app.use(express.static(PATH_TO_BUILD_DIR_PUBLIC)) 12 | 13 | const { compiler } = handleWebpackDevServer(app) 14 | 15 | handleServeBaseRouteDev({ 16 | compiler, 17 | app, 18 | }) 19 | 20 | app.listen(PORT, () => { 21 | console.info('Express is listening on PORT %s.', PORT) 22 | }) 23 | -------------------------------------------------------------------------------- /server/middleware/dev.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import path from 'path' 3 | import { Express } from 'express' 4 | import webpack, { Compiler } from 'webpack' 5 | import webpackDevMiddleware from 'webpack-dev-middleware' 6 | import webpackHotMiddleware from 'webpack-hot-middleware' 7 | 8 | export const handleWebpackDevServer = (app: Express) => { 9 | // eslint-disable-next-line 10 | const webpackDevConfig = require('../../webpack/client/dev') 11 | 12 | const compiler = webpack(webpackDevConfig) 13 | 14 | app.use( 15 | webpackDevMiddleware(compiler, { 16 | publicPath: webpackDevConfig.output.publicPath, 17 | }) 18 | ) 19 | 20 | app.use(webpackHotMiddleware(compiler, { log: false })) 21 | 22 | return { compiler } 23 | } 24 | 25 | export const handleServeBaseRouteDev = ({ 26 | compiler, 27 | app, 28 | }: { 29 | app: Express 30 | compiler: Compiler 31 | }) => { 32 | app.use('*', (req, res, next) => { 33 | const filename = path.join(compiler.outputPath, 'index.html') 34 | 35 | compiler.inputFileSystem.readFile( 36 | filename, 37 | (err: Error, result: unknown) => { 38 | if (err) { 39 | return next(err) 40 | } 41 | res.set('content-type', 'text/html') 42 | res.send(result) 43 | res.end() 44 | } 45 | ) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /server/middleware/prod.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { Express } from 'express' 3 | import compression from 'compression' 4 | import { BUILD_DIR_PUBLIC } from '../config' 5 | 6 | export const handleHttpsRedirect = (app: Express) => 7 | app.use((req, res, next) => { 8 | if ( 9 | req.hostname !== 'localhost' && 10 | req.get('X-Forwarded-Proto') !== 'https' 11 | ) { 12 | return res.redirect(`https://${req.hostname}${req.url}`) 13 | } 14 | return next() 15 | }) 16 | 17 | // TODO: Fix typescript error 18 | export const handleCompression = (app: Express) => app.use(compression() as any) 19 | 20 | export const handleServeBaseRoute = (app: Express) => 21 | app.get('*', (req, res) => { 22 | res.sendFile(path.resolve(__dirname, BUILD_DIR_PUBLIC, 'index.html')) 23 | }) 24 | -------------------------------------------------------------------------------- /server/prod.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import express from 'express' 3 | import { PATH_TO_BUILD_DIR_PUBLIC, PORT } from './config' 4 | import { 5 | handleCompression, 6 | handleHttpsRedirect, 7 | handleServeBaseRoute, 8 | } from './middleware/prod' 9 | 10 | const app = express() 11 | 12 | app.use(express.static(PATH_TO_BUILD_DIR_PUBLIC)) 13 | 14 | handleHttpsRedirect(app) 15 | handleCompression(app) 16 | handleServeBaseRoute(app) 17 | 18 | app.listen(PORT, () => { 19 | console.info('Express is listening on PORT %s.', PORT) 20 | }) 21 | -------------------------------------------------------------------------------- /src/apolloClient.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient } from 'apollo-client' 2 | import { createHttpLink } from 'apollo-link-http' 3 | import { setContext } from 'apollo-link-context' 4 | import { InMemoryCache } from 'apollo-cache-inmemory' 5 | import { ApolloLink } from 'apollo-link' 6 | import { onError } from 'apollo-link-error' 7 | import { auth } from 'services/auth' 8 | import { handleRefreshToken } from 'services/token' 9 | import { SERVER_URL } from 'config' 10 | 11 | const httpLink = createHttpLink({ 12 | uri: SERVER_URL, 13 | }) 14 | 15 | const authLink = setContext((_, { headers }) => { 16 | const token = auth.getAccessToken() 17 | 18 | return { 19 | headers: { 20 | ...headers, 21 | authorization: token ? `Bearer ${token}` : '', 22 | }, 23 | } 24 | }) 25 | 26 | export const apolloClient: ApolloClient = new ApolloClient({ 27 | link: ApolloLink.from([ 28 | onError(({ forward, graphQLErrors, operation }) => { 29 | if ( 30 | graphQLErrors.length && 31 | graphQLErrors[0].message === 'Token Expired' 32 | ) { 33 | return handleRefreshToken({ 34 | client: apolloClient, 35 | forward, 36 | operation, 37 | }) 38 | } 39 | }), 40 | authLink, 41 | httpLink, 42 | ]), 43 | cache: new InMemoryCache(), 44 | }) 45 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { GlobalStyles } from 'styles' 3 | import { Navigation } from 'components/Navigation' 4 | import { Layout } from 'components/Layout' 5 | import { Content } from 'components/Content' 6 | import { Routes } from 'routes' 7 | import 'antd/dist/antd.css' 8 | 9 | export const App = () => ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | -------------------------------------------------------------------------------- /src/appHistory.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history' 2 | 3 | export const browserHistory = createBrowserHistory() 4 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { Button as AntButton } from 'antd' 3 | import { ButtonProps } from 'antd/lib/button' 4 | 5 | export const Button: FC = ({ children, ...rest }) => ( 6 | 7 | {children} 8 | 9 | ) 10 | -------------------------------------------------------------------------------- /src/components/Content/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Layout } from 'antd' 3 | 4 | export const Content = styled(Layout.Content)` 5 | min-height: 100%; 6 | padding: 65px 50px; 7 | ` 8 | -------------------------------------------------------------------------------- /src/components/Empty/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Empty as AntEmpty } from 'antd' 3 | 4 | export const COMPONENT_EMPTY_TEST_ID = 'error-alert-component' 5 | 6 | export const Empty = () => ( 7 | 11 | ) 12 | -------------------------------------------------------------------------------- /src/components/ErrorAlert/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { Alert } from './styled' 3 | 4 | export const COMPONENT_ERROR_ALERT_TEST_ID = 'error-alert-component' 5 | 6 | export const ErrorAlert: FC = ({ children }) => ( 7 | 12 | ) 13 | -------------------------------------------------------------------------------- /src/components/ErrorAlert/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Alert as AntAlert } from 'antd' 3 | 4 | export const Alert = styled(AntAlert)` 5 | padding: 25px !important; 6 | font-size: 16px !important; 7 | ` 8 | -------------------------------------------------------------------------------- /src/components/FormButton/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Button } from '../Button' 3 | 4 | export const FormButton = styled(Button)` 5 | margin-top: 20px; 6 | ` 7 | -------------------------------------------------------------------------------- /src/components/FormElementDescription/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Description = styled.span` 4 | font-size: 14px; 5 | color: rgba(0, 0, 0, 0.25); 6 | font-variant: tabular-nums; 7 | display: block; 8 | line-height: 16px; 9 | margin-top: 10px; 10 | ` 11 | -------------------------------------------------------------------------------- /src/components/FormElementError/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const FormElementError = styled.span` 4 | font-size: 14px; 5 | line-height: 1.5; 6 | color: #f5222d; 7 | display: block; 8 | margin-top: 5px; 9 | ` 10 | -------------------------------------------------------------------------------- /src/components/FormElementLabel/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Form } from 'antd' 3 | 4 | export const FormElementLabel = styled(Form.Item)<{ isHidden?: boolean }>` 5 | display: ${props => (props.isHidden ? 'none' : 'block')}; 6 | margin-bottom: 0 !important; 7 | 8 | & + & { 9 | display: block; 10 | margin-top: 15px; 11 | } 12 | ` 13 | -------------------------------------------------------------------------------- /src/components/FormElementLink/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const ElementLink = styled.div` 4 | margin: 10px 0; 5 | ` 6 | -------------------------------------------------------------------------------- /src/components/Layout/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Layout as AntLayout } from 'antd' 3 | 4 | export const Layout = styled(AntLayout)` 5 | min-height: 100% !important; 6 | ` 7 | -------------------------------------------------------------------------------- /src/components/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Spin } from 'antd' 3 | 4 | export const COMPONENT_LOADER_TEST_ID = 'loader-component' 5 | 6 | export const Loader = () => ( 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /src/components/Navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Layout, Menu } from 'antd' 3 | import { Link, withRouter } from 'react-router-dom' 4 | import { useQuery } from '@apollo/react-hooks' 5 | import { StyledMenu } from './styled' 6 | import { Me } from 'modules/auth/gql/__generated__/Me' 7 | import { ME_QUERY } from 'modules/auth/gql' 8 | import { ROUTE_PATHS } from 'routes' 9 | 10 | const { Header } = Layout 11 | 12 | export const Navigation = withRouter(props => { 13 | const { data } = useQuery(ME_QUERY) 14 | 15 | const menuItems = [ 16 | { id: '1', to: ROUTE_PATHS.home, label: 'Home' }, 17 | ...(data?.me 18 | ? [ 19 | { id: '2', to: ROUTE_PATHS.blog.create, label: 'Create' }, 20 | { id: '3', to: ROUTE_PATHS.auth.me, label: 'Me' }, 21 | { id: '4', to: ROUTE_PATHS.auth.logout, label: 'Log Out' }, 22 | ] 23 | : [ 24 | { id: '5', to: ROUTE_PATHS.auth.register, label: 'Register' }, 25 | { id: '6', to: ROUTE_PATHS.auth.login, label: 'Login' }, 26 | ]), 27 | ] 28 | 29 | const selectedItem = menuItems.find( 30 | menuItem => menuItem.to === props.location.pathname 31 | ) 32 | const selectedIndex = selectedItem ? selectedItem.id : '0' 33 | 34 | return ( 35 |
36 | 37 | {menuItems.map(menuItem => ( 38 | 39 | {menuItem.label} 40 | 41 | ))} 42 | 43 |
44 | ) 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/Navigation/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Menu } from 'antd' 3 | 4 | export const StyledMenu = styled(Menu)` 5 | line-height: 64px !important; 6 | ` 7 | -------------------------------------------------------------------------------- /src/components/TextInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { ErrorMessage, Field, FieldProps } from 'formik' 3 | import { FormElementError } from '../FormElementError' 4 | import { FormElementLabel } from '../FormElementLabel' 5 | import { Input, Icon } from './styled' 6 | import { Description } from 'components/FormElementDescription' 7 | 8 | interface IProps { 9 | iconType?: string 10 | name: string 11 | label?: string 12 | type?: 'password' | 'hidden' | 'number' 13 | description?: string 14 | } 15 | 16 | export const TextInput: FC = ({ 17 | iconType, 18 | name, 19 | label, 20 | type, 21 | description, 22 | }) => ( 23 | ) => ( 29 | <> 30 | 33 | 44 | {description && {description}} 45 | 46 | {message}} 49 | /> 50 | 51 | )} 52 | /> 53 | ) 54 | -------------------------------------------------------------------------------- /src/components/TextInput/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Input as AntInput, Icon as AntIcon } from 'antd' 3 | 4 | export const Input = styled(AntInput)` 5 | margin-top: 5px !important; 6 | ` 7 | 8 | export const Icon = styled(AntIcon)` 9 | color: rgba(0, 0, 0, 0.25) !important; 10 | ` 11 | -------------------------------------------------------------------------------- /src/components/Textarea/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { ErrorMessage, Field, FieldProps } from 'formik' 3 | import { FormElementError } from '../FormElementError' 4 | import { FormElementLabel } from '../FormElementLabel' 5 | import { StyledTextArea } from './styled' 6 | 7 | interface IProps { 8 | name: string 9 | label: string 10 | } 11 | 12 | export const Textarea: FC = ({ name, label }) => ( 13 | 14 | {label} 15 | : 16 | ) => ( 19 | 24 | )} 25 | /> 26 | {message}} 29 | /> 30 | 31 | ) 32 | -------------------------------------------------------------------------------- /src/components/Textarea/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Input as AntInput } from 'antd' 3 | 4 | export const StyledTextArea = styled(AntInput.TextArea)` 5 | margin-top: 5px !important; 6 | ` 7 | -------------------------------------------------------------------------------- /src/components/Typography/H1.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'antd' 2 | import React, { FC } from 'react' 3 | 4 | export const H1: FC = ({ children }) => ( 5 | {children} 6 | ) 7 | -------------------------------------------------------------------------------- /src/components/Typography/H3.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'antd' 2 | import React, { FC } from 'react' 3 | 4 | export const H3: FC = ({ children }) => ( 5 | {children} 6 | ) 7 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const { SERVER_URL } = process.env 2 | -------------------------------------------------------------------------------- /src/globalTypes.ts: -------------------------------------------------------------------------------- 1 | /* Tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | //============================================================== 6 | // START Enums and Input Objects 7 | //============================================================== 8 | 9 | export interface ChangePasswordInput { 10 | password: string 11 | token: string 12 | } 13 | 14 | export interface CreatePageInput { 15 | text: string 16 | title: string 17 | } 18 | 19 | export interface RegisterInput { 20 | email: string 21 | firstName: string 22 | lastName: string 23 | password: string 24 | } 25 | 26 | export interface UpdatePageInput { 27 | id: number 28 | text: string 29 | title: string 30 | } 31 | 32 | //============================================================== 33 | // END Enums and Input Objects 34 | //============================================================== 35 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | React Webpack 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Router } from 'react-router-dom' 4 | import { ApolloProvider } from '@apollo/react-hooks' 5 | import { App } from 'app' 6 | import { apolloClient } from 'apolloClient' 7 | import { browserHistory } from 'appHistory' 8 | 9 | const renderApp = () => { 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | document.querySelector('#root') 17 | ) 18 | } 19 | 20 | renderApp() 21 | 22 | if (module.hot) { 23 | module.hot.accept() 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/auth/forms/ChangePassword/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { message } from 'antd' 4 | import { History as RouterHistory } from 'history' 5 | import { Formik, Form } from 'formik' 6 | import * as Yup from 'yup' 7 | import { useChangePassword } from '../../hooks/useChangePassword' 8 | import { auth } from '../../../../services/auth' 9 | import { TextInput } from 'components/TextInput' 10 | import { FormButton } from 'components/FormButton' 11 | import { ElementLink } from 'components/FormElementLink' 12 | import { ROUTE_PATHS } from 'routes' 13 | 14 | const initialValues = { 15 | password: '', 16 | } 17 | 18 | const forgotPasswordSchema = Yup.object().shape({ 19 | token: Yup.string().required(), 20 | password: Yup.string().required('Required'), 21 | }) 22 | 23 | interface IProps { 24 | token: string 25 | routerHistory: RouterHistory 26 | } 27 | 28 | export const ChangePasswordForm: FC = ({ token, routerHistory }) => { 29 | const [changePassword] = useChangePassword() 30 | 31 | return ( 32 | { 38 | const result = await changePassword({ 39 | variables: { data: { ...values } }, 40 | }) 41 | 42 | if (result?.data.changePassword) { 43 | await message.success( 44 | 'Password reset was successful. You were automatically logged in.', 45 | 10 46 | ) 47 | auth.logIn( 48 | result.data.changePassword.accessToken, 49 | result.data.changePassword.refreshToken 50 | ) 51 | routerHistory.push('/me') 52 | } else { 53 | setSubmitting(false) 54 | await message.error('Invalid password reset token.') 55 | } 56 | }} 57 | validationSchema={forgotPasswordSchema} 58 | > 59 | {({ isSubmitting }) => ( 60 |
61 | 62 | 63 | 64 | {isSubmitting ? 'Submitting...' : 'Submit'} 65 | 66 | 67 | Or you can login! 68 | 69 | 70 | )} 71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /src/modules/auth/forms/ForgotPassword/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { message } from 'antd' 4 | import { Formik, Form } from 'formik' 5 | import * as Yup from 'yup' 6 | import { useForgotPassword } from '../../hooks/useForgotPassword' 7 | import { TextInput } from 'components/TextInput' 8 | import { FormButton } from 'components/FormButton' 9 | import { ElementLink } from 'components/FormElementLink' 10 | import { ROUTE_PATHS } from 'routes' 11 | 12 | export const SUCCESS_MESSAGE = 'Password reset link has been sent to your email' 13 | 14 | const initialValues = { 15 | email: '', 16 | } 17 | 18 | const forgotPasswordSchema = Yup.object().shape({ 19 | email: Yup.string() 20 | .email('Invalid email') 21 | .required('Required'), 22 | }) 23 | 24 | export const ForgotPasswordForm = () => { 25 | const [resetPassword] = useForgotPassword() 26 | 27 | return ( 28 | { 31 | try { 32 | const result = await resetPassword({ variables: { ...values } }) 33 | if (result) { 34 | await message.success(SUCCESS_MESSAGE, 15) 35 | resetForm() 36 | } 37 | } catch (error) { 38 | setSubmitting(false) 39 | await message.error(error.message) 40 | } 41 | }} 42 | validationSchema={forgotPasswordSchema} 43 | > 44 | {({ isSubmitting }) => ( 45 |
46 | 47 | 48 | {isSubmitting ? 'Submitting...' : 'Submit'} 49 | 50 | 51 | Or you can login! 52 | 53 | 54 | )} 55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/modules/auth/forms/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { Formik, Form } from 'formik' 4 | import * as Yup from 'yup' 5 | import { History as RouterHistory } from 'history' 6 | import { useLogin } from '../../hooks/useLogin' 7 | import { previousLocation } from '../../../router/previousLocation' 8 | import { showAllGraphQLErrors } from 'services/messages' 9 | import { auth } from 'services/auth' 10 | import { TextInput } from 'components/TextInput' 11 | import { FormButton } from 'components/FormButton' 12 | import { ElementLink } from 'components/FormElementLink' 13 | import { ROUTE_PATHS } from 'routes' 14 | 15 | const initialValues = { 16 | email: 'email@email1.com', 17 | password: 'heslo1234', 18 | } 19 | 20 | const loginSchema = Yup.object().shape({ 21 | email: Yup.string(), 22 | password: Yup.string(), 23 | }) 24 | 25 | interface IProps { 26 | routerHistory: RouterHistory 27 | } 28 | 29 | export const LoginForm: FC = ({ routerHistory }) => { 30 | const [login] = useLogin() 31 | 32 | return ( 33 | { 36 | try { 37 | const result = await login({ variables: { ...values } }) 38 | if (result) { 39 | auth.logIn( 40 | result.data.login.accessToken, 41 | result.data.login.refreshToken 42 | ) 43 | const targetPath = previousLocation(routerHistory.location) 44 | routerHistory.push(targetPath) 45 | } 46 | } catch (error) { 47 | setSubmitting(false) 48 | showAllGraphQLErrors(error.graphQLErrors) 49 | } 50 | }} 51 | validationSchema={loginSchema} 52 | > 53 | {({ isSubmitting }) => ( 54 |
55 | 56 | 62 | 63 | Forgot password 64 | 65 | 66 | {isSubmitting ? 'Logging in...' : 'Login'} 67 | 68 | 69 | Or register now! 70 | 71 | 72 | )} 73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/modules/auth/forms/Register/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { Formik, Form } from 'formik' 4 | import * as Yup from 'yup' 5 | import { History as RouterHistory } from 'history' 6 | import { useRegister } from '../../hooks/useRegister' 7 | import { showAllGraphQLErrors } from 'services/messages' 8 | import { auth } from 'services/auth' 9 | import { ROUTE_PATHS } from 'routes' 10 | import { TextInput } from 'components/TextInput' 11 | import { FormButton } from 'components/FormButton' 12 | import { ElementLink } from 'components/FormElementLink' 13 | 14 | const initialValues = { 15 | email: '', 16 | password: '', 17 | firstName: 'John', 18 | lastName: 'Doe', 19 | } 20 | 21 | const registerSchema = Yup.object().shape({ 22 | email: Yup.string() 23 | .email('Invalid email') 24 | .required('Required'), 25 | password: Yup.string() 26 | .min(5, 'Too Short!') 27 | .max(70, 'Too Long!') 28 | .required('Required'), 29 | firstName: Yup.string().required('Required'), 30 | lastName: Yup.string().required('Required'), 31 | }) 32 | 33 | interface IProps { 34 | routerHistory: RouterHistory 35 | } 36 | 37 | export const RegisterForm: FC = ({ routerHistory }) => { 38 | const [register] = useRegister() 39 | 40 | return ( 41 | { 44 | try { 45 | const result = await register({ variables: { data: values } }) 46 | if (result) { 47 | auth.logIn( 48 | result.data.register.accessToken, 49 | result.data.register.refreshToken 50 | ) 51 | routerHistory.push('/me') 52 | } 53 | } catch (error) { 54 | setSubmitting(false) 55 | showAllGraphQLErrors(error.graphQLErrors) 56 | } 57 | }} 58 | validationSchema={registerSchema} 59 | > 60 | {({ isSubmitting }) => ( 61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | {isSubmitting ? 'Registering...' : 'Register'} 69 | 70 | 71 | Or you can use existing account to{' '} 72 | login! 73 | 74 | 75 | )} 76 |
77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /src/modules/auth/gql/__generated__/AccessToken.ts: -------------------------------------------------------------------------------- 1 | /* Tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL query operation: AccessToken 7 | // ==================================================== 8 | 9 | export interface AccessToken { 10 | accessToken: string 11 | } 12 | 13 | export interface AccessTokenVariables { 14 | refreshToken: string 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/auth/gql/__generated__/ChangePassword.ts: -------------------------------------------------------------------------------- 1 | /* Tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { ChangePasswordInput } from './../../../../globalTypes' 6 | 7 | // ==================================================== 8 | // GraphQL mutation operation: ChangePassword 9 | // ==================================================== 10 | 11 | export interface ChangePassword_changePassword { 12 | __typename: 'Session' 13 | accessToken: string 14 | refreshToken: string 15 | } 16 | 17 | export interface ChangePassword { 18 | changePassword: ChangePassword_changePassword | null 19 | } 20 | 21 | export interface ChangePasswordVariables { 22 | data: ChangePasswordInput 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/auth/gql/__generated__/ForgotPassword.ts: -------------------------------------------------------------------------------- 1 | /* Tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: ForgotPassword 7 | // ==================================================== 8 | 9 | export interface ForgotPassword { 10 | forgotPassword: boolean 11 | } 12 | 13 | export interface ForgotPasswordVariables { 14 | email: string 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/auth/gql/__generated__/Login.ts: -------------------------------------------------------------------------------- 1 | /* Tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: Login 7 | // ==================================================== 8 | 9 | export interface Login_login { 10 | __typename: 'Session' 11 | accessToken: string 12 | refreshToken: string 13 | } 14 | 15 | export interface Login { 16 | login: Login_login 17 | } 18 | 19 | export interface LoginVariables { 20 | email: string 21 | password: string 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/auth/gql/__generated__/Me.ts: -------------------------------------------------------------------------------- 1 | /* Tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL query operation: Me 7 | // ==================================================== 8 | 9 | export interface Me_me_pages { 10 | __typename: 'Page' 11 | id: string 12 | title: string 13 | text: string 14 | } 15 | 16 | export interface Me_me { 17 | __typename: 'User' 18 | id: string 19 | email: string 20 | firstName: string 21 | lastName: string 22 | pages: Me_me_pages[] | null 23 | } 24 | 25 | export interface Me { 26 | me: Me_me | null 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/auth/gql/__generated__/Register.ts: -------------------------------------------------------------------------------- 1 | /* Tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { RegisterInput } from './../../../../globalTypes' 6 | 7 | // ==================================================== 8 | // GraphQL mutation operation: Register 9 | // ==================================================== 10 | 11 | export interface Register_register { 12 | __typename: 'Session' 13 | accessToken: string 14 | refreshToken: string 15 | } 16 | 17 | export interface Register { 18 | register: Register_register 19 | } 20 | 21 | export interface RegisterVariables { 22 | data: RegisterInput 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/auth/gql/index.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | 3 | export const ME_QUERY = gql` 4 | query Me { 5 | me { 6 | id 7 | email 8 | firstName 9 | lastName 10 | pages { 11 | id 12 | title 13 | text 14 | } 15 | } 16 | } 17 | ` 18 | 19 | export const REGISTER_MUTATION = gql` 20 | mutation Register($data: RegisterInput!) { 21 | register(data: $data) { 22 | accessToken 23 | refreshToken 24 | } 25 | } 26 | ` 27 | 28 | export const LOGIN_MUTATION = gql` 29 | mutation Login($email: String!, $password: String!) { 30 | login(email: $email, password: $password) { 31 | accessToken 32 | refreshToken 33 | } 34 | } 35 | ` 36 | 37 | export const FORGOT_PASSWORD_MUTATION = gql` 38 | mutation ForgotPassword($email: String!) { 39 | forgotPassword(email: $email) 40 | } 41 | ` 42 | 43 | export const CHANGE_PASSWORD_MUTATION = gql` 44 | mutation ChangePassword($data: ChangePasswordInput!) { 45 | changePassword(data: $data) { 46 | accessToken 47 | refreshToken 48 | } 49 | } 50 | ` 51 | 52 | export const ACCESS_TOKEN_QUERY = gql` 53 | query AccessToken($refreshToken: String!) { 54 | accessToken(refreshToken: $refreshToken) 55 | } 56 | ` 57 | -------------------------------------------------------------------------------- /src/modules/auth/hooks/useChangePassword.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@apollo/react-hooks' 2 | import { CHANGE_PASSWORD_MUTATION } from '../gql' 3 | import { 4 | ChangePassword, 5 | ChangePasswordVariables, 6 | } from '../gql/__generated__/ChangePassword' 7 | 8 | export const useChangePassword = () => 9 | useMutation(CHANGE_PASSWORD_MUTATION) 10 | -------------------------------------------------------------------------------- /src/modules/auth/hooks/useForgotPassword.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@apollo/react-hooks' 2 | import { FORGOT_PASSWORD_MUTATION } from '../gql' 3 | import { 4 | ForgotPassword, 5 | ForgotPasswordVariables, 6 | } from '../gql/__generated__/ForgotPassword' 7 | 8 | export const useForgotPassword = () => 9 | useMutation(FORGOT_PASSWORD_MUTATION) 10 | -------------------------------------------------------------------------------- /src/modules/auth/hooks/useLogin.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@apollo/react-hooks' 2 | import { LOGIN_MUTATION } from '../gql' 3 | import { Login, LoginVariables } from '../gql/__generated__/Login' 4 | 5 | export const useLogin = () => useMutation(LOGIN_MUTATION) 6 | -------------------------------------------------------------------------------- /src/modules/auth/hooks/useMe.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/react-hooks' 2 | import { ME_QUERY } from '../gql' 3 | import { Me } from '../gql/__generated__/Me' 4 | 5 | export const useMe = () => useQuery(ME_QUERY) 6 | -------------------------------------------------------------------------------- /src/modules/auth/hooks/useRegister.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@apollo/react-hooks' 2 | import { Register, RegisterVariables } from '../gql/__generated__/Register' 3 | import { REGISTER_MUTATION } from '../gql' 4 | 5 | export const useRegister = () => 6 | useMutation(REGISTER_MUTATION) 7 | -------------------------------------------------------------------------------- /src/modules/blog/cache/updateListPages.ts: -------------------------------------------------------------------------------- 1 | import { DataProxy } from 'apollo-cache' 2 | import { ListPages } from '../gql/__generated__/ListPages' 3 | import { LIST_PAGES_QUERY } from '../gql' 4 | 5 | export const updateListPages = ( 6 | queryName: string, 7 | callBack: ( 8 | listPages: ListPages['listPages'], 9 | result: T 10 | ) => ListPages['listPages'] 11 | ) => (cache: DataProxy, params: any) => { 12 | const result = params.data[queryName] 13 | 14 | const { listPages } = cache.readQuery({ 15 | query: LIST_PAGES_QUERY, 16 | }) 17 | cache.writeQuery({ 18 | query: LIST_PAGES_QUERY, 19 | data: { listPages: callBack(listPages, result) }, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/blog/components/PageCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import LinesEllipsis from 'react-lines-ellipsis' 3 | import { ListPages_listPages } from '../../gql/__generated__/ListPages' 4 | import { Card, CardLink } from './styled' 5 | 6 | interface IProps { 7 | page: Omit 8 | } 9 | 10 | export const COMPONENT_PAGE_CARD_TEST_ID = 'page-card-component' 11 | 12 | export const PageCard: FC = ({ page }) => ( 13 | 18 | 19 | Read More 20 | 21 | ) 22 | -------------------------------------------------------------------------------- /src/modules/blog/components/PageCard/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Card as AntCard } from 'antd' 3 | import { Link } from 'react-router-dom' 4 | 5 | export const Card = styled(AntCard)` 6 | margin-top: 0 !important; 7 | 8 | & + & { 9 | margin-top: 25px; 10 | } 11 | ` 12 | 13 | export const CardLink = styled(Link)` 14 | margin-top: 25px; 15 | display: block; 16 | ` 17 | -------------------------------------------------------------------------------- /src/modules/blog/components/PagesList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useQuery } from '@apollo/react-hooks' 3 | import { PageCard } from '../PageCard' 4 | import { Container } from './styled' 5 | import { LIST_PAGES_QUERY } from 'modules/blog/gql' 6 | import { ListPages } from 'modules/blog/gql/__generated__/ListPages' 7 | import { Loader } from 'components/Loader' 8 | import { Empty } from 'components/Empty' 9 | import { ErrorAlert } from 'components/ErrorAlert' 10 | 11 | export const PagesList = React.memo(() => { 12 | const { data, loading, error } = useQuery(LIST_PAGES_QUERY) 13 | 14 | if (error) { 15 | return {error.message} 16 | } 17 | 18 | if (loading) { 19 | return 20 | } 21 | 22 | if (!data.listPages.length) { 23 | return 24 | } 25 | 26 | return ( 27 | 28 | {data.listPages.map(page => ( 29 | 30 | ))} 31 | 32 | ) 33 | }) 34 | -------------------------------------------------------------------------------- /src/modules/blog/components/PagesList/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Container = styled.div` 4 | > div + div { 5 | margin-top: 25px !important; 6 | } 7 | ` 8 | -------------------------------------------------------------------------------- /src/modules/blog/components/RelevantPagesList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { PageCard } from '../PageCard' 3 | import { ListPages_listPages } from '../../gql/__generated__/ListPages' 4 | import { PageDetail_pageDetail_user } from '../../gql/__generated__/PageDetail' 5 | import { Container, PagesContainer } from './styled' 6 | import { H3 } from 'components/Typography/H3' 7 | 8 | interface IProps { 9 | pages: Array> 10 | user?: Omit 11 | title?: string 12 | } 13 | 14 | export const RelevantPagesList: FC = ({ pages, user, title }) => ( 15 | 16 |

{title || `More from ${user.email}`}

17 | 18 | {pages.map(page => ( 19 | 20 | ))} 21 | 22 |
23 | ) 24 | -------------------------------------------------------------------------------- /src/modules/blog/components/RelevantPagesList/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { mediaQueries } from 'styles/mediaQueries' 3 | 4 | export const Container = styled.div` 5 | margin-top: 50px; 6 | ` 7 | 8 | export const PagesContainer = styled.div` 9 | display: grid; 10 | grid-template-columns: 1fr; 11 | grid-auto-rows: 1fr; 12 | grid-column-gap: 30px; 13 | grid-row-gap: 30px; 14 | 15 | ${mediaQueries.md} { 16 | grid-template-columns: repeat(2, 1fr); 17 | } 18 | 19 | ${mediaQueries.lg} { 20 | grid-template-columns: repeat(3, 1fr); 21 | } 22 | ` 23 | -------------------------------------------------------------------------------- /src/modules/blog/forms/Page/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { Formik, Form, FormikHelpers } from 'formik' 3 | import * as Yup from 'yup' 4 | import { TextInput } from 'components/TextInput' 5 | import { Textarea } from 'components/Textarea' 6 | import { FormButton } from 'components/FormButton' 7 | 8 | const registerSchema = Yup.object().shape({ 9 | title: Yup.string().required('Required'), 10 | text: Yup.string().required('Required'), 11 | }) 12 | 13 | export interface IPageFormValues { 14 | id?: number 15 | title: string 16 | text: string 17 | } 18 | 19 | export interface IProps { 20 | initialValues?: IPageFormValues 21 | handleSubmit: ( 22 | values: IPageFormValues, 23 | actions?: FormikHelpers 24 | ) => Promise 25 | } 26 | 27 | export const PageForm: FC = ({ initialValues, handleSubmit }) => ( 28 | 33 | {({ isSubmitting }) => ( 34 |
35 | 36 |