Full Code of oldboyxx/jira_clone for AI

master 26a9e77b1789 cached
209 files
276.0 KB
85.0k tokens
30 symbols
1 requests
Download .txt
Showing preview only (322K chars total). Download the full file or copy to clipboard to get everything.
Repository: oldboyxx/jira_clone
Branch: master
Commit: 26a9e77b1789
Files: 209
Total size: 276.0 KB

Directory structure:
gitextract_pfax6a_i/

├── .gitignore
├── .vscode/
│   └── settings.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── api/
│   ├── .eslintignore
│   ├── .eslintrc.json
│   ├── .prettierrc
│   ├── README.md
│   ├── package.json
│   ├── src/
│   │   ├── constants/
│   │   │   ├── issues.ts
│   │   │   └── projects.ts
│   │   ├── controllers/
│   │   │   ├── authentication.ts
│   │   │   ├── comments.ts
│   │   │   ├── issues.ts
│   │   │   ├── projects.ts
│   │   │   ├── test.ts
│   │   │   └── users.ts
│   │   ├── database/
│   │   │   ├── createConnection.ts
│   │   │   ├── createGuestAccount.ts
│   │   │   ├── createTestAccount.ts
│   │   │   └── resetDatabase.ts
│   │   ├── entities/
│   │   │   ├── Comment.ts
│   │   │   ├── Issue.ts
│   │   │   ├── Project.ts
│   │   │   ├── User.ts
│   │   │   └── index.ts
│   │   ├── errors/
│   │   │   ├── asyncCatch.ts
│   │   │   ├── customErrors.ts
│   │   │   └── index.ts
│   │   ├── index.ts
│   │   ├── middleware/
│   │   │   ├── authentication.ts
│   │   │   ├── errors.ts
│   │   │   └── response.ts
│   │   ├── routes.ts
│   │   ├── serializers/
│   │   │   └── issues.ts
│   │   ├── types/
│   │   │   ├── env.d.ts
│   │   │   └── express.d.ts
│   │   └── utils/
│   │       ├── authToken.ts
│   │       ├── typeorm.ts
│   │       └── validation.ts
│   ├── tsconfig-paths.js
│   └── tsconfig.json
├── client/
│   ├── .babelrc
│   ├── .eslintrc.json
│   ├── .prettierrc
│   ├── README.md
│   ├── cypress/
│   │   ├── .eslintrc.json
│   │   ├── integration/
│   │   │   ├── authentication.spec.js
│   │   │   ├── issueCreate.spec.js
│   │   │   ├── issueDetails.spec.js
│   │   │   ├── issueFilters.spec.js
│   │   │   ├── issueSearch.spec.js
│   │   │   ├── issuesDragDrop.spec.js
│   │   │   └── projectSettings.spec.js
│   │   ├── plugins/
│   │   │   └── index.js
│   │   └── support/
│   │       ├── commands.js
│   │       ├── index.js
│   │       └── utils.js
│   ├── cypress.json
│   ├── jest/
│   │   ├── fileMock.js
│   │   └── styleMock.js
│   ├── jest.config.js
│   ├── jsconfig.json
│   ├── package.json
│   ├── server.js
│   ├── src/
│   │   ├── App/
│   │   │   ├── BaseStyles.js
│   │   │   ├── NormalizeStyles.js
│   │   │   ├── Routes.jsx
│   │   │   ├── Toast/
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── assets/
│   │   │   │   └── fonts/
│   │   │   │       ├── CircularStd-Black.otf
│   │   │   │       ├── CircularStd-Bold.otf
│   │   │   │       ├── CircularStd-Book.otf
│   │   │   │       └── CircularStd-Medium.otf
│   │   │   ├── fontStyles.css
│   │   │   └── index.jsx
│   │   ├── Auth/
│   │   │   └── Authenticate.jsx
│   │   ├── Project/
│   │   │   ├── Board/
│   │   │   │   ├── Filters/
│   │   │   │   │   ├── Styles.js
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── Header/
│   │   │   │   │   ├── Styles.js
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── IssueDetails/
│   │   │   │   │   ├── AssigneesReporter/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Comments/
│   │   │   │   │   │   ├── BodyForm/
│   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   ├── Comment/
│   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   ├── Create/
│   │   │   │   │   │   │   ├── ProTip/
│   │   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Dates/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Delete.jsx
│   │   │   │   │   ├── Description/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── EstimateTracking/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   ├── TrackingWidget/
│   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Loader.jsx
│   │   │   │   │   ├── Priority/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Status/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Styles.js
│   │   │   │   │   ├── Title/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Type/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── Lists/
│   │   │   │   │   ├── List/
│   │   │   │   │   │   ├── Issue/
│   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Styles.js
│   │   │   │   │   └── index.jsx
│   │   │   │   └── index.jsx
│   │   │   ├── IssueCreate/
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── IssueSearch/
│   │   │   │   ├── NoResultsSvg.jsx
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── NavbarLeft/
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── ProjectSettings/
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── Sidebar/
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── Styles.js
│   │   │   └── index.jsx
│   │   ├── browserHistory.js
│   │   ├── index.html
│   │   ├── index.jsx
│   │   └── shared/
│   │       ├── components/
│   │       │   ├── AboutTooltip/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Avatar/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Breadcrumbs/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Button/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── ConfirmModal/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── CopyLinkButton.jsx
│   │       │   ├── DatePicker/
│   │       │   │   ├── DateSection.jsx
│   │       │   │   ├── Styles.js
│   │       │   │   ├── TimeSection.jsx
│   │       │   │   └── index.jsx
│   │       │   ├── Form/
│   │       │   │   ├── Field.jsx
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Icon/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Input/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── InputDebounced.jsx
│   │       │   ├── IssuePriorityIcon/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── IssueTypeIcon/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Logo.jsx
│   │       │   ├── Modal/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── PageError/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── PageLoader/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── ProjectAvatar.jsx
│   │       │   ├── Select/
│   │       │   │   ├── Dropdown.jsx
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Spinner.jsx
│   │       │   ├── TextEditedContent/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── TextEditor/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Textarea/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Tooltip/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   └── index.js
│   │       ├── constants/
│   │       │   ├── issues.js
│   │       │   ├── keyCodes.js
│   │       │   └── projects.js
│   │       ├── hooks/
│   │       │   ├── api/
│   │       │   │   ├── index.js
│   │       │   │   ├── mutation.js
│   │       │   │   └── query.js
│   │       │   ├── currentUser.js
│   │       │   ├── deepCompareMemoize.js
│   │       │   ├── mergeState.js
│   │       │   ├── onEscapeKeyDown.js
│   │       │   └── onOutsideClick.js
│   │       └── utils/
│   │           ├── api.js
│   │           ├── authToken.js
│   │           ├── browser.js
│   │           ├── dateTime.js
│   │           ├── javascript.js
│   │           ├── queryParamModal.js
│   │           ├── styles.js
│   │           ├── toast.js
│   │           ├── url.js
│   │           └── validation.js
│   ├── webpack.config.js
│   └── webpack.config.production.js
└── package.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# dependencies
**/node_modules

# misc
**/.DS_Store

# environment config
**/.env

# production
**/build
**/npm-debug.log*


================================================
FILE: .vscode/settings.json
================================================
{
  "eslint.workingDirectories": [
    {
      "directory": "./client",
      "changeProcessCWD": true
    },
    {
      "directory": "./api",
      "changeProcessCWD": true
    }
  ]
}


================================================
FILE: CONTRIBUTING.md
================================================
I will not be accepting PR's on this repository. Feel free to fork and maintain your own version.


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2013-present, Yuxi (Evan) You

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
<h1 align="center">A simplified Jira clone built with React and Node</h1>

<div align="center">Auto formatted with Prettier, tested with Cypress 🎗</div>

<h3 align="center">
  <a href="https://jira.ivorreic.com/">Visit the live app</a> |
  <a href="https://github.com/oldboyxx/jira_clone/tree/master/client">View client</a> |
  <a href="https://github.com/oldboyxx/jira_clone/tree/master/api">View API</a>
</h3>

![Tech logos](https://i.ibb.co/DVFj8PL/tech-icons.jpg)

![App screenshot](https://i.ibb.co/W3qVvCn/jira-optimized.jpg)

## What is this and who is it for 🤷‍♀️

I do React consulting and this is a showcase product I've built in my spare time. It's a very good example of modern, real-world React codebase.

There are many showcase/example React projects out there but most of them are way too simple. I like to think that this codebase contains enough complexity to offer valuable insights to React developers of all skill levels while still being _relatively_ easy to understand.

## Features

- Proven, scalable, and easy to understand project structure
- Written in modern React, only functional components with hooks
- A variety of custom light-weight UI components such as datepicker, modal, various form elements etc
- Simple local React state management, without redux, mobx, or similar
- Custom webpack setup, without create-react-app or similar
- Client written in Babel powered JavaScript
- API written in TypeScript and using TypeORM

## Setting up development environment 🛠

- Install [postgreSQL](https://www.postgresql.org/) if you don't have it already and create a database named `jira_development`.
- `git clone https://github.com/oldboyxx/jira_clone.git`
- Create an empty `.env` file in `/api`, copy `/api/.env.example` contents into it, and fill in your database username and password.
- `npm run install-dependencies`
- `cd api && npm start`
- `cd client && npm start` in another terminal tab
- App should now be running on `http://localhost:8080/`

## Running cypress end-to-end tests 🚥

- Set up development environment
- Create a database named `jira_test` and start the api with `cd api && npm run start:test`
- `cd client && npm run test:cypress`

## What's missing?

There are features missing from this showcase product which should exist in a real product:

### Migrations 🗄

We're currently using TypeORM's `synchronize` feature which auto creates the database schema on every application launch. It's fine to do this in a showcase product or during early development while the product is not used by anyone, but before going live with a real product, we should [introduce migrations](https://github.com/typeorm/typeorm/blob/master/docs/migrations.md).

### Proper authentication system 🔐

We currently auto create an auth token and seed a project with issues and users for anyone who visits the API without valid credentials. In a real product we'd want to implement a proper [email and password authentication system](https://www.google.com/search?q=email+and+password+authentication+node+js&oq=email+and+password+authentication+node+js).

### Accessibility ♿

Not all components have properly defined [aria attributes](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA), visual focus indicators etc. Most early stage companies tend to ignore this aspect of their product but in many cases they shouldn't, especially once their userbase starts growing.

### Unit/Integration tests 🧪

Both Client and API are currently tested through [end-to-end Cypress tests](https://github.com/oldboyxx/jira_clone/tree/master/client/cypress/integration). That's good enough for a relatively simple application such as this, even if it was a real product. However, as the app grows in complexity, it might be wise to start writing additional unit/integration tests.

## Contributing

I will not be accepting PR's on this repository. Feel free to fork and maintain your own version.

## License

[MIT](https://opensource.org/licenses/MIT)

<hr>

<h3>
  <a href="https://jira.ivorreic.com/">Visit the live app</a> |
  <a href="https://github.com/oldboyxx/jira_clone/tree/master/client">View client</a> |
  <a href="https://github.com/oldboyxx/jira_clone/tree/master/api">View API</a>
</h3>


================================================
FILE: api/.eslintignore
================================================
build/*
tsconfig-paths.js


================================================
FILE: api/.eslintrc.json
================================================
{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json",
    "sourceType": "module",
    "ecmaVersion": 8
  },
  "plugins": ["@typescript-eslint"],
  "env": {
    "node": true
  },
  "extends": [
    "airbnb-base",
    "plugin:import/typescript",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  "rules": {
    "radix": 0,
    "no-restricted-syntax": 0,
    "no-await-in-loop": 0,
    "no-console": 0,
    "consistent-return": 0,
    "@typescript-eslint/no-unused-vars": 0,
    "@typescript-eslint/no-use-before-define": 0,
    "@typescript-eslint/no-explicit-any": 0,
    "import/prefer-default-export": 0,
    "import/no-cycle": 0
  },
  "settings": {
    // Allows us to lint absolute imports within codebase
    "import/resolver": {
      "node": {
        "moduleDirectory": ["node_modules", "src/"]
      }
    }
  }
}


================================================
FILE: api/.prettierrc
================================================
{
  "printWidth": 100,
  "singleQuote": true,
  "trailingComma": "all"
}


================================================
FILE: api/README.md
================================================
# Project structure 🏗

The API codebase is fairly simple and should be easy enough to understand.

<br>

| File or folder    | Description                                                                                                                                                                                                                 |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/index.ts`    | The entry file. This is where we setup middleware, attach routes, initialize database and express.                                                                                                                          |
| `src/routes.ts`   | This is where we define all routes, both public and private.                                                                                                                                                                |
| `src/constants`   | Constants are values that never change and are used in multiple places across the codebase.                                                                                                                                 |
| `src/controllers` | Controllers listen to client's requests and work with entities and the database to fetch, add, update, or delete data.                                                                                                      |
| `src/database`    | Database related code and seeds go here.                                                                                                                                                                                    |
| `src/entities`    | This is where we put TypeORM entities, you could think of them as models. We define columns, relations, validations for each database entity.                                                                               |
| `src/errors`      | This is where we define custom errors. The `catchErrors` function helps us avoid repetitive `try/catch` blocks within controllers.                                                                                          |
| `src/middleware`  | Middleware functions can modify request and response objects, end the request-response cycle, etc. For example `authenticateUser` method verifies the authorization token and attaches `currentUser` to the request object. |
| `src/serializers` | Serializers transform the data fetched from the database before it's sent to the client.                                                                                                                                    |
| `src/utils`       | Utility(helper) functions that are used in multiple places across the codebase. For example `utils/typeorm.ts` functions help us validate data and avoid writing repetitive code.                                           |


================================================
FILE: api/package.json
================================================
{
  "name": "jira_api",
  "version": "1.0.0",
  "author": "Ivor Reic",
  "license": "MIT",
  "scripts": {
    "start": "nodemon --exec ts-node --files src/index.ts",
    "start:test": "cross-env NODE_ENV='test' DB_DATABASE='jira_test' npm start",
    "start:production": "pm2 start --name 'jira_api' node -- -r ./tsconfig-paths.js build/index.js",
    "build": "cd src && tsc",
    "pre-commit": "lint-staged"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "express-async-handler": "^1.1.4",
    "faker": "^4.1.0",
    "jsonwebtoken": "^8.5.1",
    "lodash": "^4.17.15",
    "module-alias": "^2.2.2",
    "pg": "^7.14.0",
    "reflect-metadata": "^0.1.13",
    "striptags": "^3.1.1",
    "typeorm": "^0.2.20"
  },
  "devDependencies": {
    "@types/cors": "^2.8.6",
    "@types/express": "^4.17.2",
    "@types/faker": "^4.1.7",
    "@types/jsonapi-serializer": "^3.6.2",
    "@types/jsonwebtoken": "^8.3.5",
    "@types/lodash": "^4.14.149",
    "@types/node": "^12.12.11",
    "@typescript-eslint/eslint-plugin": "^2.7.0",
    "@typescript-eslint/parser": "^2.7.0",
    "cross-env": "^6.0.3",
    "eslint": "^6.1.0",
    "eslint-config-airbnb-base": "^14.0.0",
    "eslint-config-prettier": "^6.7.0",
    "eslint-plugin-import": "^2.18.2",
    "eslint-plugin-prettier": "^3.1.1",
    "lint-staged": "^9.4.3",
    "nodemon": "^2.0.0",
    "prettier": "^1.19.1",
    "ts-node": "^8.5.2",
    "tsconfig-paths": "^3.9.0",
    "typescript": "^3.7.2"
  },
  "_moduleDirectories": [
    "src"
  ],
  "lint-staged": {
    "*.ts": [
      "eslint --fix",
      "prettier --write",
      "git add"
    ]
  }
}


================================================
FILE: api/src/constants/issues.ts
================================================
export enum IssueType {
  TASK = 'task',
  BUG = 'bug',
  STORY = 'story',
}

export enum IssueStatus {
  BACKLOG = 'backlog',
  SELECTED = 'selected',
  INPROGRESS = 'inprogress',
  DONE = 'done',
}

export enum IssuePriority {
  HIGHEST = '5',
  HIGH = '4',
  MEDIUM = '3',
  LOW = '2',
  LOWEST = '1',
}


================================================
FILE: api/src/constants/projects.ts
================================================
export enum ProjectCategory {
  SOFTWARE = 'software',
  MARKETING = 'marketing',
  BUSINESS = 'business',
}


================================================
FILE: api/src/controllers/authentication.ts
================================================
import { catchErrors } from 'errors';
import { signToken } from 'utils/authToken';
import createAccount from 'database/createGuestAccount';

export const createGuestAccount = catchErrors(async (_req, res) => {
  const user = await createAccount();
  res.respond({
    authToken: signToken({ sub: user.id }),
  });
});


================================================
FILE: api/src/controllers/comments.ts
================================================
import { Comment } from 'entities';
import { catchErrors } from 'errors';
import { updateEntity, deleteEntity, createEntity } from 'utils/typeorm';

export const create = catchErrors(async (req, res) => {
  const comment = await createEntity(Comment, req.body);
  res.respond({ comment });
});

export const update = catchErrors(async (req, res) => {
  const comment = await updateEntity(Comment, req.params.commentId, req.body);
  res.respond({ comment });
});

export const remove = catchErrors(async (req, res) => {
  const comment = await deleteEntity(Comment, req.params.commentId);
  res.respond({ comment });
});


================================================
FILE: api/src/controllers/issues.ts
================================================
import { Issue } from 'entities';
import { catchErrors } from 'errors';
import { updateEntity, deleteEntity, createEntity, findEntityOrThrow } from 'utils/typeorm';

export const getProjectIssues = catchErrors(async (req, res) => {
  const { projectId } = req.currentUser;
  const { searchTerm } = req.query;

  let whereSQL = 'issue.projectId = :projectId';

  if (searchTerm) {
    whereSQL += ' AND (issue.title ILIKE :searchTerm OR issue.descriptionText ILIKE :searchTerm)';
  }

  const issues = await Issue.createQueryBuilder('issue')
    .select()
    .where(whereSQL, { projectId, searchTerm: `%${searchTerm}%` })
    .getMany();

  res.respond({ issues });
});

export const getIssueWithUsersAndComments = catchErrors(async (req, res) => {
  const issue = await findEntityOrThrow(Issue, req.params.issueId, {
    relations: ['users', 'comments', 'comments.user'],
  });
  res.respond({ issue });
});

export const create = catchErrors(async (req, res) => {
  const listPosition = await calculateListPosition(req.body);
  const issue = await createEntity(Issue, { ...req.body, listPosition });
  res.respond({ issue });
});

export const update = catchErrors(async (req, res) => {
  const issue = await updateEntity(Issue, req.params.issueId, req.body);
  res.respond({ issue });
});

export const remove = catchErrors(async (req, res) => {
  const issue = await deleteEntity(Issue, req.params.issueId);
  res.respond({ issue });
});

const calculateListPosition = async ({ projectId, status }: Issue): Promise<number> => {
  const issues = await Issue.find({ projectId, status });

  const listPositions = issues.map(({ listPosition }) => listPosition);

  if (listPositions.length > 0) {
    return Math.min(...listPositions) - 1;
  }
  return 1;
};


================================================
FILE: api/src/controllers/projects.ts
================================================
import { Project } from 'entities';
import { catchErrors } from 'errors';
import { findEntityOrThrow, updateEntity } from 'utils/typeorm';
import { issuePartial } from 'serializers/issues';

export const getProjectWithUsersAndIssues = catchErrors(async (req, res) => {
  const project = await findEntityOrThrow(Project, req.currentUser.projectId, {
    relations: ['users', 'issues'],
  });
  res.respond({
    project: {
      ...project,
      issues: project.issues.map(issuePartial),
    },
  });
});

export const update = catchErrors(async (req, res) => {
  const project = await updateEntity(Project, req.currentUser.projectId, req.body);
  res.respond({ project });
});


================================================
FILE: api/src/controllers/test.ts
================================================
import { catchErrors } from 'errors';
import { signToken } from 'utils/authToken';
import resetTestDatabase from 'database/resetDatabase';
import createTestAccount from 'database/createTestAccount';

export const resetDatabase = catchErrors(async (_req, res) => {
  await resetTestDatabase();
  res.respond(true);
});

export const createAccount = catchErrors(async (_req, res) => {
  const user = await createTestAccount();
  res.respond({
    authToken: signToken({ sub: user.id }),
  });
});


================================================
FILE: api/src/controllers/users.ts
================================================
import { catchErrors } from 'errors';

export const getCurrentUser = catchErrors((req, res) => {
  res.respond({ currentUser: req.currentUser });
});


================================================
FILE: api/src/database/createConnection.ts
================================================
import { createConnection, Connection } from 'typeorm';

import * as entities from 'entities';

const createDatabaseConnection = (): Promise<Connection> =>
  createConnection({
    type: 'postgres',
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    username: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_DATABASE,
    entities: Object.values(entities),
    synchronize: true,
  });

export default createDatabaseConnection;


================================================
FILE: api/src/database/createGuestAccount.ts
================================================
import { Comment, Issue, Project, User } from 'entities';
import { ProjectCategory } from 'constants/projects';
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
import { createEntity } from 'utils/typeorm';

const seedUsers = (): Promise<User[]> => {
  const users = [
    createEntity(User, {
      email: 'rick@jira.guest',
      name: 'Pickle Rick',
      avatarUrl: 'https://i.ibb.co/7JM1P2r/picke-rick.jpg',
    }),
    createEntity(User, {
      email: 'yoda@jira.guest',
      name: 'Baby Yoda',
      avatarUrl: 'https://i.ibb.co/6n0hLML/baby-yoda.jpg',
    }),
    createEntity(User, {
      email: 'gaben@jira.guest',
      name: 'Lord Gaben',
      avatarUrl: 'https://i.ibb.co/6RJ5hq6/gaben.jpg',
    }),
  ];
  return Promise.all(users);
};

const seedProject = (users: User[]): Promise<Project> =>
  createEntity(Project, {
    name: 'singularity 1.0',
    url: 'https://www.atlassian.com/software/jira',
    description:
      'Plan, track, and manage your agile and software development projects in Jira. Customize your workflow, collaborate, and release great software.',
    category: ProjectCategory.SOFTWARE,
    users,
  });

const seedIssues = (project: Project): Promise<Issue[]> => {
  const { users } = project;

  const issues = [
    createEntity(Issue, {
      title: 'This is an issue of type: Task.',
      type: IssueType.TASK,
      status: IssueStatus.BACKLOG,
      priority: IssuePriority.HIGH,
      listPosition: 1,
      description: `<p>Your teams can collaborate in Jira applications by breaking down pieces of work into issues. Issues can represent tasks, software bugs, feature requests or any other type of project work.</p><p><br></p><h3>Jira Software&nbsp;(software projects) issue types:</h3><p><br></p><h1><strong>Bug </strong><span style="background-color: initial;">🐞</span></h1><p>A bug is a problem which impairs or prevents the functions of a product.</p><p><br></p><h1><strong>Story </strong><span style="color: rgb(51, 51, 51);">📗</span></h1><p>A user story is the smallest unit of work that needs to be done.</p><p><br></p><h1><strong>Task </strong><span style="color: rgb(51, 51, 51);">🗳</span></h1><p>A task represents work that needs to be done.</p>`,
      estimate: 8,
      timeSpent: 4,
      reporterId: users[1].id,
      project,
      users: [users[0]],
    }),
    createEntity(Issue, {
      title: "Click on an issue to see what's behind it.",
      type: IssueType.TASK,
      status: IssueStatus.BACKLOG,
      priority: IssuePriority.LOW,
      listPosition: 2,
      description: `<h2>Key terms to know</h2><p><br></p><h3>Issues</h3><p>A Jira 'issue' refers to a single work item of any type or size that is tracked from creation to completion. For example, an issue could be a feature being developed by a software team, a to-do item for a marketing team, or a contract that needs to be written by a legal team.</p><p><br></p><h3>Projects</h3><p>A project is, quite simply, a collection of issues that are held in common by purpose or context. Issues grouped into projects can be configured in a variety of ways, ranging from visibility restrictions to available workflows.</p><p><br></p><h3>Workflows</h3><p>Workflows represent the&nbsp;sequential&nbsp;path an issues takes from creation to completion. A basic workflow might look something like this:</p><p><br></p><p><img src="https://wac-cdn.atlassian.com/dam/jcr:6203a73b-f2a1-4d91-9587-bc4b7d822d6b/workflow_timeline_desktop-temporary.svg?cdnVersion=736" alt="Jira workflow diagram"></p><p><br></p><p>In this case, Open, Done, and the labels in between represent the status an issue can take, while the arrows represent potential transitions from one status to another.</p><p><br></p><h3>Agile</h3><p>Agile is not a Jira Software-specific term. It's a work philosophy that originated in the software development field and has since expanded to a variety of other industries. While we won't belabor the definition here (there are&nbsp;great resources for that!), agile emphasizes an iterative approach to work informed by customer feedback where delivery occurs incrementally and continuously. The ideal agile team can move quickly and adapt to changing requirements without missing much of a beat.</p><p><br></p><h3>Server</h3><p>With&nbsp;Jira Software Server,&nbsp;you host Jira Software on your own hardware and customize your setup however you'd like. This is generally the best option for teams who need to manage all the details,&nbsp;have stricter requirements for data governance,&nbsp;and don't mind the additional complexity of hosting themselves.</p><p><br></p><p><img src="https://wac-cdn.atlassian.com/dam/jcr:4a1b934f-38b4-456e-b807-29e93935e00f/Server%20Cluster@2x.png?cdnVersion=736" alt="Data Center"></p><p><br></p><h3>Data Center</h3><p><br></p><h3>With&nbsp;Jira Software Data Center, you can host Jira Software on your own hardware or with IaaS vendors like&nbsp;<a href="https://www.atlassian.com/enterprise/data-center/aws" rel="noopener noreferrer" target="_blank">AWS</a>&nbsp;and&nbsp;<a href="https://www.atlassian.com/enterprise/data-center/azure" rel="noopener noreferrer" target="_blank">Azure</a>. This is generally the best option for enterprise teams who need uninterrupted access to Jira Software and performance at scale.</h3><p><br></p>`,
      estimate: 5,
      timeSpent: 2,
      reporterId: users[2].id,
      project,
      users: [users[0]],
    }),
    createEntity(Issue, {
      title: 'Try dragging issues to different columns to transition their status.',
      type: IssueType.STORY,
      status: IssueStatus.BACKLOG,
      priority: IssuePriority.MEDIUM,
      listPosition: 3,
      description: `<p>An issue's status indicates its current place in the project's workflow. Here's a list of the statuses that come with&nbsp;JIRA products, depending on what projects you've created on your site.</p><p><br></p><h3>Jira software issue statuses:</h3><p><br></p><h2><strong style="background-color: rgb(187, 187, 187);"> Backlog </strong></h2><p>The issue is waiting to be picked up in a future sprint.</p><p><br></p><h2><strong style="background-color: rgb(187, 187, 187);"> Selected </strong></h2><p>The issue is open and ready for the assignee to start work on it.</p><p><br></p><h2><strong style="background-color: rgb(0, 102, 204); color: rgb(255, 255, 255);"> In Progress </strong></h2><p>This issue is being actively worked on at the moment by the assignee.</p><p><br></p><h2><strong style="background-color: rgb(0, 138, 0); color: rgb(255, 255, 255);"> Done </strong></h2><p>Work has finished on the issue.</p>`,
      estimate: 15,
      timeSpent: 12,
      reporterId: users[1].id,
      project,
    }),
    createEntity(Issue, {
      title: 'You can use rich text with images in issue descriptions.',
      type: IssueType.STORY,
      status: IssueStatus.BACKLOG,
      priority: IssuePriority.LOWEST,
      listPosition: 4,
      description: `<h1><span style="color: rgb(51, 51, 51);">🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🍈 🍒 🍑 🍍 🥭 🥥 🥝 🍅 🍆 🥑 🥦 🥒 🥬 🌶 🌽 🥕 🥔 🍠 🥐 🍞 🥖 🥨 🥯 🧀 🥚 🍳 🥞 🥓 🥩 🍗 🍖 🌭 🍔 🍟 🍕 🥪 🥙 🌮 🌯 🥗 🥘 🥫 🍝 🍜 🍲 🍛 🍣 🍱 🥟 🍤 🍙 🍚 🍘 🍥 🥮 🥠 🍢 🍡 🍧 🍨 🍦 🥧 🍰 🎂 🍮 🍭 🍬 🍫 🍿 🧂 🍩 🍪 🌰 🥜 🍯 🥛 🍼 ☕️ 🍵 🥤 🍶 🍺 🍻 🥂 🍷 🥃 🍸 🍹 🍾 🥄 🍴 🍽 🥣 🥡 🥢</span></h1>`,
      estimate: 4,
      timeSpent: 4,
      reporterId: users[0].id,
      project,
      users: [users[2]],
    }),
    createEntity(Issue, {
      title: 'Each issue can be assigned priority from lowest to highest.',
      type: IssueType.TASK,
      status: IssueStatus.SELECTED,
      priority: IssuePriority.HIGHEST,
      listPosition: 5,
      description: `<p>An issue's priority indicates its relative importance. The default priorities are listed below. Both the priorities and their meanings can be&nbsp;customized by your administrator to suit your organization.&nbsp;<a href="https://confluence.atlassian.com/adminjiracloud/configuring-statuses-resolutions-and-priorities-776636333.html" rel="noopener noreferrer" target="_blank">Learn more about configuring priorities and their descriptions</a>.</p><p><br></p><h3>Jira software issue priorities:</h3><p><br></p><h3><strong style="background-color: rgb(230, 0, 0); color: rgb(255, 255, 255);"> Highest </strong><strong style="color: rgb(255, 255, 255);"> </strong><span style="color: rgb(51, 51, 51);">⬆️</span></h3><p>This problem will block progress.</p><p><br></p><h3><strong style="background-color: rgb(240, 102, 102); color: rgb(255, 255, 255);"> High </strong><strong style="color: rgb(255, 255, 255);"> </strong><span style="color: rgb(51, 51, 51);">⬆️</span></h3><p>Serious problem that could block progress.</p><p><br></p><h3><strong style="background-color: rgb(255, 153, 0); color: rgb(255, 255, 255);"> Medium </strong><strong style="color: rgb(255, 255, 255);"> </strong><span style="color: rgb(51, 51, 51);">⬆️</span></h3><p>Has the potential to affect progress.</p><p><br></p><h3><strong style="background-color: rgb(0, 138, 0); color: rgb(255, 255, 255);"> Low </strong><strong style="color: rgb(255, 255, 255);"> </strong><span style="color: rgb(51, 51, 51);">⬇️</span></h3><p>Minor problem or easily worked around.</p><p><br></p><h3><strong style="background-color: rgb(102, 185, 102); color: rgb(255, 255, 255);"> Lowest </strong><strong style="color: rgb(255, 255, 255);"> </strong><span style="color: rgb(51, 51, 51);">⬇️</span></h3><p>Trivial problem with little or no impact on progress.</p>`,
      estimate: 4,
      timeSpent: 1,
      reporterId: users[2].id,
      project,
    }),
    createEntity(Issue, {
      title: 'Each issue has a single reporter but can have multiple assignees.',
      type: IssueType.STORY,
      status: IssueStatus.SELECTED,
      priority: IssuePriority.HIGH,
      listPosition: 6,
      description: `<h2>Try assigning <u style="background-color: rgb(204, 232, 204);">Pickle Rick</u> to this issue. <span style="color: rgb(51, 51, 51);">🥒&nbsp;🥒&nbsp;🥒</span></h2><p><br></p>`,
      estimate: 6,
      timeSpent: 3,
      reporterId: users[1].id,
      project,
      users: [users[1], users[2]],
    }),
    createEntity(Issue, {
      title:
        'You can track how many hours were spent working on an issue, and how many hours remain.',
      type: IssueType.TASK,
      status: IssueStatus.INPROGRESS,
      priority: IssuePriority.LOWEST,
      listPosition: 7,
      description: `<p>Before you start work on an issue, you can set a time or other type of estimate to calculate how much work you believe it'll take to resolve it. Once you've started to work on a specific issue, log time to keep a record of it.</p><p><br></p><ul><li>Open the issue and select&nbsp;••• &gt;&nbsp;Time tracking</li><li>Fill in the<strong>&nbsp;Time Spent</strong>&nbsp;field</li><li>Fill in the <strong>Time Remaining</strong> field and click Save</li></ul><p><br></p><h3><u style="background-color: initial;">That's it!</u></h3><h1>💯💯</h1>`,
      estimate: 12,
      timeSpent: 11,
      reporterId: users[0].id,
      project,
    }),
    createEntity(Issue, {
      title: 'Try leaving a comment on this issue.',
      type: IssueType.TASK,
      status: IssueStatus.DONE,
      priority: IssuePriority.MEDIUM,
      listPosition: 7,
      description: `<p>Adding comments to an issue is a useful way to record additional detail about an issue, and collaborate with team members. Comments are shown in the&nbsp;<strong>Comments</strong>&nbsp;section when you&nbsp;<a href="https://confluence.atlassian.com/jira064/what-is-an-issue-720416138.html" rel="noopener noreferrer" target="_blank" style="color: rgb(0, 82, 204); background-color: rgb(255, 255, 255);">view an issue</a>.</p><p><br></p><ol><li>Open the&nbsp;<a href="https://confluence.atlassian.com/jira064/what-is-an-issue-720416138.html" rel="noopener noreferrer" target="_blank" style="color: rgb(0, 82, 204);">issue</a>&nbsp;on which to add your comment.</li><li>Click the&nbsp;<strong>Add a comment</strong>&nbsp;button.</li><li class="ql-indent-1"><img src="https://confluence.atlassian.com/s/en_GB/7901/af536c7c6dffcc1d697b914b797aa7f2f306b4f8/_/images/icons/emoticons/check.svg" alt="(tick)">&nbsp;<a href="https://confluence.atlassian.com/jira064/using-keyboard-shortcuts-720416165.html#UsingKeyboardShortcuts-issues" rel="noopener noreferrer" target="_blank" style="color: rgb(0, 82, 204);">Keyboard shortcut</a>:&nbsp;<strong>m</strong></li><li>In the&nbsp;<strong>Comment</strong>&nbsp;text box, type your comment, using as many lines as you require.&nbsp;<img src="https://confluence.atlassian.com/s/en_GB/7901/af536c7c6dffcc1d697b914b797aa7f2f306b4f8/_/images/icons/emoticons/check.svg" alt="(tick)">&nbsp;</li><li>Click the&nbsp;<strong>Save</strong>&nbsp;button to save the comment.</li></ol>`,
      estimate: 10,
      timeSpent: 2,
      reporterId: users[0].id,
      project,
      users: [users[1]],
    }),
  ];
  return Promise.all(issues);
};

const seedComments = (issues: Issue[], users: User[]): Promise<Comment[]> => {
  const comments = [
    createEntity(Comment, {
      body: 'An old silent pond...\nA frog jumps into the pond,\nsplash! Silence again.',
      issueId: issues[0].id,
      userId: users[2].id,
    }),
    createEntity(Comment, {
      body: 'Autumn moonlight-\na worm digs silently\ninto the chestnut.',
      issueId: issues[1].id,
      userId: users[2].id,
    }),
    createEntity(Comment, {
      body: 'In the twilight rain\nthese brilliant-hued hibiscus -\nA lovely sunset.',
      issueId: issues[2].id,
      userId: users[2].id,
    }),
    createEntity(Comment, {
      body: 'A summer river being crossed\nhow pleasing\nwith sandals in my hands!',
      issueId: issues[3].id,
      userId: users[2].id,
    }),
    createEntity(Comment, {
      body: "Light of the moon\nMoves west, flowers' shadows\nCreep eastward.",
      issueId: issues[4].id,
      userId: users[2].id,
    }),
    createEntity(Comment, {
      body: 'In the moonlight,\nThe color and scent of the wisteria\nSeems far away.',
      issueId: issues[5].id,
      userId: users[2].id,
    }),
    createEntity(Comment, {
      body: 'O snail\nClimb Mount Fuji,\nBut slowly, slowly!',
      issueId: issues[6].id,
      userId: users[2].id,
    }),
    createEntity(Comment, {
      body: 'Everything I touch\nwith tenderness, alas,\npricks like a bramble.',
      issueId: issues[7].id,
      userId: users[2].id,
    }),
  ];
  return Promise.all(comments);
};

const createGuestAccount = async (): Promise<User> => {
  const users = await seedUsers();
  const project = await seedProject(users);
  const issues = await seedIssues(project);
  await seedComments(issues, project.users);
  return users[2];
};

export default createGuestAccount;


================================================
FILE: api/src/database/createTestAccount.ts
================================================
import { Comment, Issue, Project, User } from 'entities';
import { ProjectCategory } from 'constants/projects';
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
import { createEntity } from 'utils/typeorm';

const seedUsers = (): Promise<User[]> => {
  const users = [
    createEntity(User, {
      email: 'gaben@jira.test',
      name: 'Gaben',
      avatarUrl: 'https://i.ibb.co/6RJ5hq6/gaben.jpg',
    }),
    createEntity(User, {
      email: 'yoda@jira.test',
      name: 'Yoda',
      avatarUrl: 'https://i.ibb.co/6n0hLML/baby-yoda.jpg',
    }),
  ];
  return Promise.all(users);
};

const seedProject = (users: User[]): Promise<Project> =>
  createEntity(Project, {
    name: 'Project name',
    url: 'https://www.testurl.com',
    description: 'Project description',
    category: ProjectCategory.SOFTWARE,
    users,
  });

const seedIssues = (project: Project): Promise<Issue[]> => {
  const { users } = project;

  const issues = [
    createEntity(Issue, {
      title: 'Issue title 1',
      type: IssueType.TASK,
      status: IssueStatus.BACKLOG,
      priority: IssuePriority.LOWEST,
      listPosition: 1,
      reporterId: users[0].id,
      project,
    }),
    createEntity(Issue, {
      title: 'Issue title 2',
      type: IssueType.TASK,
      status: IssueStatus.BACKLOG,
      priority: IssuePriority.MEDIUM,
      listPosition: 2,
      estimate: 5,
      description: 'Issue description 2',
      reporterId: users[0].id,
      users: [users[0]],
      project,
    }),
    createEntity(Issue, {
      title: 'Issue title 3',
      type: IssueType.STORY,
      status: IssueStatus.SELECTED,
      priority: IssuePriority.HIGH,
      listPosition: 3,
      estimate: 10,
      description: 'Issue description 3',
      reporterId: users[0].id,
      users: [users[0], users[1]],
      project,
    }),
  ];
  return Promise.all(issues);
};

const seedComments = (issue: Issue, user: User): Promise<Comment> =>
  createEntity(Comment, {
    body: 'Comment body',
    issueId: issue.id,
    userId: user.id,
  });

const createTestAccount = async (): Promise<User> => {
  const users = await seedUsers();
  const project = await seedProject(users);
  const issues = await seedIssues(project);
  await seedComments(issues[0], project.users[0]);
  return users[0];
};

export default createTestAccount;


================================================
FILE: api/src/database/resetDatabase.ts
================================================
import { getConnection } from 'typeorm';

const resetDatabase = async (): Promise<void> => {
  const connection = getConnection();
  await connection.dropDatabase();
  await connection.synchronize();
};

export default resetDatabase;


================================================
FILE: api/src/entities/Comment.ts
================================================
import {
  BaseEntity,
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToOne,
} from 'typeorm';

import is from 'utils/validation';
import { Issue, User } from '.';

@Entity()
class Comment extends BaseEntity {
  static validations = {
    body: [is.required(), is.maxLength(50000)],
  };

  @PrimaryGeneratedColumn()
  id: number;

  @Column('text')
  body: string;

  @CreateDateColumn({ type: 'timestamp' })
  createdAt: Date;

  @UpdateDateColumn({ type: 'timestamp' })
  updatedAt: Date;

  @ManyToOne(
    () => User,
    user => user.comments,
  )
  user: User;

  @Column('integer')
  userId: number;

  @ManyToOne(
    () => Issue,
    issue => issue.comments,
    { onDelete: 'CASCADE' },
  )
  issue: Issue;

  @Column('integer')
  issueId: number;
}

export default Comment;


================================================
FILE: api/src/entities/Issue.ts
================================================
import striptags from 'striptags';
import {
  BaseEntity,
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToOne,
  OneToMany,
  ManyToMany,
  JoinTable,
  RelationId,
  BeforeUpdate,
  BeforeInsert,
} from 'typeorm';

import is from 'utils/validation';
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
import { Comment, Project, User } from '.';

@Entity()
class Issue extends BaseEntity {
  static validations = {
    title: [is.required(), is.maxLength(200)],
    type: [is.required(), is.oneOf(Object.values(IssueType))],
    status: [is.required(), is.oneOf(Object.values(IssueStatus))],
    priority: [is.required(), is.oneOf(Object.values(IssuePriority))],
    listPosition: is.required(),
    reporterId: is.required(),
  };

  @PrimaryGeneratedColumn()
  id: number;

  @Column('varchar')
  title: string;

  @Column('varchar')
  type: IssueType;

  @Column('varchar')
  status: IssueStatus;

  @Column('varchar')
  priority: IssuePriority;

  @Column('double precision')
  listPosition: number;

  @Column('text', { nullable: true })
  description: string | null;

  @Column('text', { nullable: true })
  descriptionText: string | null;

  @Column('integer', { nullable: true })
  estimate: number | null;

  @Column('integer', { nullable: true })
  timeSpent: number | null;

  @Column('integer', { nullable: true })
  timeRemaining: number | null;

  @CreateDateColumn({ type: 'timestamp' })
  createdAt: Date;

  @UpdateDateColumn({ type: 'timestamp' })
  updatedAt: Date;

  @Column('integer')
  reporterId: number;

  @ManyToOne(
    () => Project,
    project => project.issues,
  )
  project: Project;

  @Column('integer')
  projectId: number;

  @OneToMany(
    () => Comment,
    comment => comment.issue,
  )
  comments: Comment[];

  @ManyToMany(
    () => User,
    user => user.issues,
  )
  @JoinTable()
  users: User[];

  @RelationId((issue: Issue) => issue.users)
  userIds: number[];

  @BeforeInsert()
  @BeforeUpdate()
  setDescriptionText = (): void => {
    if (this.description) {
      this.descriptionText = striptags(this.description);
    }
  };
}

export default Issue;


================================================
FILE: api/src/entities/Project.ts
================================================
import {
  BaseEntity,
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  OneToMany,
} from 'typeorm';

import is from 'utils/validation';
import { ProjectCategory } from 'constants/projects';
import { Issue, User } from '.';

@Entity()
class Project extends BaseEntity {
  static validations = {
    name: [is.required(), is.maxLength(100)],
    url: is.url(),
    category: [is.required(), is.oneOf(Object.values(ProjectCategory))],
  };

  @PrimaryGeneratedColumn()
  id: number;

  @Column('varchar')
  name: string;

  @Column('varchar', { nullable: true })
  url: string | null;

  @Column('text', { nullable: true })
  description: string | null;

  @Column('varchar')
  category: ProjectCategory;

  @CreateDateColumn({ type: 'timestamp' })
  createdAt: Date;

  @UpdateDateColumn({ type: 'timestamp' })
  updatedAt: Date;

  @OneToMany(
    () => Issue,
    issue => issue.project,
  )
  issues: Issue[];

  @OneToMany(
    () => User,
    user => user.project,
  )
  users: User[];
}

export default Project;


================================================
FILE: api/src/entities/User.ts
================================================
import {
  BaseEntity,
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  OneToMany,
  ManyToMany,
  ManyToOne,
  RelationId,
} from 'typeorm';

import is from 'utils/validation';
import { Comment, Issue, Project } from '.';

@Entity()
class User extends BaseEntity {
  static validations = {
    name: [is.required(), is.maxLength(100)],
    email: [is.required(), is.email(), is.maxLength(200)],
  };

  @PrimaryGeneratedColumn()
  id: number;

  @Column('varchar')
  name: string;

  @Column('varchar')
  email: string;

  @Column('varchar', { length: 2000 })
  avatarUrl: string;

  @CreateDateColumn({ type: 'timestamp' })
  createdAt: Date;

  @UpdateDateColumn({ type: 'timestamp' })
  updatedAt: Date;

  @OneToMany(
    () => Comment,
    comment => comment.user,
  )
  comments: Comment[];

  @ManyToMany(
    () => Issue,
    issue => issue.users,
  )
  issues: Issue[];

  @ManyToOne(
    () => Project,
    project => project.users,
  )
  project: Project;

  @RelationId((user: User) => user.project)
  projectId: number;
}

export default User;


================================================
FILE: api/src/entities/index.ts
================================================
export { default as Comment } from './Comment';
export { default as Issue } from './Issue';
export { default as Project } from './Project';
export { default as User } from './User';


================================================
FILE: api/src/errors/asyncCatch.ts
================================================
import { RequestHandler } from 'express';

export const catchErrors = (requestHandler: RequestHandler): RequestHandler => {
  return async (req, res, next): Promise<any> => {
    try {
      return await requestHandler(req, res, next);
    } catch (error) {
      next(error);
    }
  };
};


================================================
FILE: api/src/errors/customErrors.ts
================================================
/* eslint-disable max-classes-per-file */

type ErrorData = { [key: string]: any };

export class CustomError extends Error {
  constructor(
    public message: string,
    public code: string | number = 'INTERNAL_ERROR',
    public status: number = 500,
    public data: ErrorData = {},
  ) {
    super();
  }
}

export class RouteNotFoundError extends CustomError {
  constructor(originalUrl: string) {
    super(`Route '${originalUrl}' does not exist.`, 'ROUTE_NOT_FOUND', 404);
  }
}

export class EntityNotFoundError extends CustomError {
  constructor(entityName: string) {
    super(`${entityName} not found.`, 'ENTITY_NOT_FOUND', 404);
  }
}

export class BadUserInputError extends CustomError {
  constructor(errorData: ErrorData) {
    super('There were validation errors.', 'BAD_USER_INPUT', 400, errorData);
  }
}

export class InvalidTokenError extends CustomError {
  constructor(message = 'Authentication token is invalid.') {
    super(message, 'INVALID_TOKEN', 401);
  }
}


================================================
FILE: api/src/errors/index.ts
================================================
export * from './customErrors';
export { catchErrors } from './asyncCatch';


================================================
FILE: api/src/index.ts
================================================
import 'module-alias/register';
import 'dotenv/config';
import 'reflect-metadata';
import express from 'express';
import cors from 'cors';

import createDatabaseConnection from 'database/createConnection';
import { addRespondToResponse } from 'middleware/response';
import { authenticateUser } from 'middleware/authentication';
import { handleError } from 'middleware/errors';
import { RouteNotFoundError } from 'errors';

import { attachPublicRoutes, attachPrivateRoutes } from './routes';

const establishDatabaseConnection = async (): Promise<void> => {
  try {
    await createDatabaseConnection();
  } catch (error) {
    console.log(error);
  }
};

const initializeExpress = (): void => {
  const app = express();

  app.use(cors());
  app.use(express.json());
  app.use(express.urlencoded());

  app.use(addRespondToResponse);

  attachPublicRoutes(app);

  app.use('/', authenticateUser);

  attachPrivateRoutes(app);

  app.use((req, _res, next) => next(new RouteNotFoundError(req.originalUrl)));
  app.use(handleError);

  app.listen(process.env.PORT || 3000);
};

const initializeApp = async (): Promise<void> => {
  await establishDatabaseConnection();
  initializeExpress();
};

initializeApp();


================================================
FILE: api/src/middleware/authentication.ts
================================================
import { Request } from 'express';

import { verifyToken } from 'utils/authToken';
import { catchErrors, InvalidTokenError } from 'errors';
import { User } from 'entities';

export const authenticateUser = catchErrors(async (req, _res, next) => {
  const token = getAuthTokenFromRequest(req);
  if (!token) {
    throw new InvalidTokenError('Authentication token not found.');
  }
  const userId = verifyToken(token).sub;
  if (!userId) {
    throw new InvalidTokenError('Authentication token is invalid.');
  }
  const user = await User.findOne(userId);
  if (!user) {
    throw new InvalidTokenError('Authentication token is invalid: User not found.');
  }
  req.currentUser = user;
  next();
});

const getAuthTokenFromRequest = (req: Request): string | null => {
  const header = req.get('Authorization') || '';
  const [bearer, token] = header.split(' ');
  return bearer === 'Bearer' && token ? token : null;
};


================================================
FILE: api/src/middleware/errors.ts
================================================
import { ErrorRequestHandler } from 'express';
import { pick } from 'lodash';

import { CustomError } from 'errors';

export const handleError: ErrorRequestHandler = (error, _req, res, _next) => {
  console.error(error);

  const isErrorSafeForClient = error instanceof CustomError;

  const clientError = isErrorSafeForClient
    ? pick(error, ['message', 'code', 'status', 'data'])
    : {
        message: 'Something went wrong, please contact our support.',
        code: 'INTERNAL_ERROR',
        status: 500,
        data: {},
      };

  res.status(clientError.status).send({ error: clientError });
};


================================================
FILE: api/src/middleware/response.ts
================================================
import { RequestHandler } from 'express';

export const addRespondToResponse: RequestHandler = (_req, res, next) => {
  res.respond = (data): void => {
    res.status(200).send(data);
  };
  next();
};


================================================
FILE: api/src/routes.ts
================================================
import * as authentication from 'controllers/authentication';
import * as comments from 'controllers/comments';
import * as issues from 'controllers/issues';
import * as projects from 'controllers/projects';
import * as test from 'controllers/test';
import * as users from 'controllers/users';

export const attachPublicRoutes = (app: any): void => {
  if (process.env.NODE_ENV === 'test') {
    app.delete('/test/reset-database', test.resetDatabase);
    app.post('/test/create-account', test.createAccount);
  }

  app.post('/authentication/guest', authentication.createGuestAccount);
};

export const attachPrivateRoutes = (app: any): void => {
  app.post('/comments', comments.create);
  app.put('/comments/:commentId', comments.update);
  app.delete('/comments/:commentId', comments.remove);

  app.get('/issues', issues.getProjectIssues);
  app.get('/issues/:issueId', issues.getIssueWithUsersAndComments);
  app.post('/issues', issues.create);
  app.put('/issues/:issueId', issues.update);
  app.delete('/issues/:issueId', issues.remove);

  app.get('/project', projects.getProjectWithUsersAndIssues);
  app.put('/project', projects.update);

  app.get('/currentUser', users.getCurrentUser);
};


================================================
FILE: api/src/serializers/issues.ts
================================================
import { pick } from 'lodash';

import { Issue } from 'entities';

export const issuePartial = (issue: Issue): Partial<Issue> =>
  pick(issue, [
    'id',
    'title',
    'type',
    'status',
    'priority',
    'listPosition',
    'createdAt',
    'updatedAt',
    'userIds',
  ]);


================================================
FILE: api/src/types/env.d.ts
================================================
declare namespace NodeJS {
  export interface ProcessEnv {
    DB_HOST: string;
    DB_PORT: string;
    DB_USERNAME: string;
    DB_PASSWORD: string;
    DB_DATABASE: string;
    JWT_SECRET: string;
  }
}


================================================
FILE: api/src/types/express.d.ts
================================================
declare namespace Express {
  export interface Response {
    respond: (data: any) => void;
  }
  export interface Request {
    currentUser: import('entities').User;
  }
}


================================================
FILE: api/src/utils/authToken.ts
================================================
import jwt, { SignOptions } from 'jsonwebtoken';
import { isPlainObject } from 'lodash';

import { InvalidTokenError } from 'errors';

export const signToken = (payload: object, options?: SignOptions): string =>
  jwt.sign(payload, process.env.JWT_SECRET, {
    expiresIn: '180 days',
    ...options,
  });

export const verifyToken = (token: string): { [key: string]: any } => {
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);

    if (isPlainObject(payload)) {
      return payload as { [key: string]: any };
    }
    throw new Error();
  } catch (error) {
    throw new InvalidTokenError();
  }
};


================================================
FILE: api/src/utils/typeorm.ts
================================================
import { FindOneOptions } from 'typeorm/find-options/FindOneOptions';

import { Project, User, Issue, Comment } from 'entities';
import { EntityNotFoundError, BadUserInputError } from 'errors';
import { generateErrors } from 'utils/validation';

type EntityConstructor = typeof Project | typeof User | typeof Issue | typeof Comment;
type EntityInstance = Project | User | Issue | Comment;

const entities: { [key: string]: EntityConstructor } = { Comment, Issue, Project, User };

export const findEntityOrThrow = async <T extends EntityConstructor>(
  Constructor: T,
  id: number | string,
  options?: FindOneOptions,
): Promise<InstanceType<T>> => {
  const instance = await Constructor.findOne(id, options);
  if (!instance) {
    throw new EntityNotFoundError(Constructor.name);
  }
  return instance;
};

export const validateAndSaveEntity = async <T extends EntityInstance>(instance: T): Promise<T> => {
  const Constructor = entities[instance.constructor.name];

  if ('validations' in Constructor) {
    const errorFields = generateErrors(instance, Constructor.validations);

    if (Object.keys(errorFields).length > 0) {
      throw new BadUserInputError({ fields: errorFields });
    }
  }
  return instance.save() as Promise<T>;
};

export const createEntity = async <T extends EntityConstructor>(
  Constructor: T,
  input: Partial<InstanceType<T>>,
): Promise<InstanceType<T>> => {
  const instance = Constructor.create(input);
  return validateAndSaveEntity(instance as InstanceType<T>);
};

export const updateEntity = async <T extends EntityConstructor>(
  Constructor: T,
  id: number | string,
  input: Partial<InstanceType<T>>,
): Promise<InstanceType<T>> => {
  const instance = await findEntityOrThrow(Constructor, id);
  Object.assign(instance, input);
  return validateAndSaveEntity(instance);
};

export const deleteEntity = async <T extends EntityConstructor>(
  Constructor: T,
  id: number | string,
): Promise<InstanceType<T>> => {
  const instance = await findEntityOrThrow(Constructor, id);
  await instance.remove();
  return instance;
};


================================================
FILE: api/src/utils/validation.ts
================================================
type Value = any;
type ErrorMessage = false | string;
type FieldValues = { [key: string]: Value };
type Validator = (value: Value, fieldValues?: FieldValues) => ErrorMessage;
type FieldValidators = { [key: string]: Validator | Validator[] };
type FieldErrors = { [key: string]: string };

const is = {
  match: (testFn: Function, message = '') => (
    value: Value,
    fieldValues: FieldValues,
  ): ErrorMessage => !testFn(value, fieldValues) && message,

  required: () => (value: Value): ErrorMessage =>
    isNilOrEmptyString(value) && 'This field is required',

  minLength: (min: number) => (value: Value): ErrorMessage =>
    !!value && value.length < min && `Must be at least ${min} characters`,

  maxLength: (max: number) => (value: Value): ErrorMessage =>
    !!value && value.length > max && `Must be at most ${max} characters`,

  oneOf: (arr: any[]) => (value: Value): ErrorMessage =>
    !!value && !arr.includes(value) && `Must be one of: ${arr.join(', ')}`,

  notEmptyArray: () => (value: Value): ErrorMessage =>
    Array.isArray(value) && value.length === 0 && 'Please add at least one item',

  email: () => (value: Value): ErrorMessage =>
    !!value && !/.+@.+\..+/.test(value) && 'Must be a valid email',

  url: () => (value: Value): ErrorMessage =>
    !!value &&
    // eslint-disable-next-line no-useless-escape
    !/^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/.test(value) &&
    'Must be a valid URL',
};

const isNilOrEmptyString = (value: Value): boolean =>
  value === undefined || value === null || value === '';

export const generateErrors = (
  fieldValues: FieldValues,
  fieldValidators: FieldValidators,
): FieldErrors => {
  const fieldErrors: FieldErrors = {};

  Object.entries(fieldValidators).forEach(([fieldName, validators]) => {
    [validators].flat().forEach(validator => {
      const errorMessage = validator(fieldValues[fieldName], fieldValues);

      if (errorMessage !== false && !fieldErrors[fieldName]) {
        fieldErrors[fieldName] = errorMessage;
      }
    });
  });
  return fieldErrors;
};

export default is;


================================================
FILE: api/tsconfig-paths.js
================================================
const tsConfigPaths = require('tsconfig-paths');

const tsConfig = require('./tsconfig.json');

// Typescript compiler doesn't rewrite absolute paths back to relative
// when compiling production code to /build. Instead we have to use
// tsconfig-paths to do that job when we run our production start script.
// https://github.com/microsoft/TypeScript/issues/10866
tsConfigPaths.register({
  baseUrl: tsConfig.compilerOptions.outDir,
  paths: tsConfig.compilerOptions.paths,
});


================================================
FILE: api/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "lib": ["dom", "es6", "es2017", "es2019", "esnext.asynciterable"],
    "sourceMap": true,
    "outDir": "./build",
    "removeComments": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": false,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": false,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "node",
    "baseUrl": "src",
    "paths": {
      "*": ["./*"]
    },
    "types": ["node"],
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "forceConsistentCasingInFileNames": true
  },
  "exclude": ["node_modules"],
  "include": ["./src/**/*.ts"]
}


================================================
FILE: client/.babelrc
================================================
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": 3
      }
    ],
    "@babel/react"
  ],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    "@babel/plugin-proposal-export-namespace-from",
    "@babel/plugin-syntax-dynamic-import",
    ["@babel/plugin-proposal-class-properties", { "loose": true }]
  ]
}


================================================
FILE: client/.eslintrc.json
================================================
{
  "parser": "babel-eslint",
  "parserOptions": {
    "sourceType": "module",
    "ecmaVersion": 8,
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "env": {
    "browser": true,
    "jest": true
  },
  "extends": ["airbnb", "prettier", "prettier/react"],
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn",
    "radix": 0,
    "no-restricted-syntax": 0,
    "no-await-in-loop": 0,
    "no-console": 0,
    "consistent-return": 0,
    "no-param-reassign": [2, { "props": false }],
    "no-return-assign": [2, "except-parens"],
    "no-use-before-define": 0,
    "import/prefer-default-export": 0,
    "import/no-cycle": 0,
    "react/no-array-index-key": 0,
    "react/forbid-prop-types": 0,
    "react/prop-types": [2, { "skipUndeclared": true }],
    "react/jsx-fragments": [2, "element"],
    "react/state-in-constructor": 0,
    "react/jsx-props-no-spreading": 0,
    "jsx-a11y/click-events-have-key-events": 0
  },
  "settings": {
    // Allows us to lint absolute imports within codebase
    "import/resolver": {
      "node": {
        "moduleDirectory": ["node_modules", "src/"]
      }
    }
  }
}


================================================
FILE: client/.prettierrc
================================================
{
  "printWidth": 100,
  "singleQuote": true,
  "trailingComma": "all"
}


================================================
FILE: client/README.md
================================================
# Project structure 🏗

I've used this architecture on multiple larger projects in the past and it performed really well.

There are two special root folders in `src`: `App` and `shared` (described below). All other root folders in `src` (in our case only two: `Auth` and `Project`) should follow the structure of the routes. We can call these folders modules.

The main rule to follow: **Files from one module can only import from ancestor folders within the same module or from `src/shared`.** This makes the codebase easier to understand, and if you're fiddling with code in one module, you will never introduce a bug in another module.

<br>

| File or folder   | Description                                                                                                                                                                                          |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/index.jsx`  | The entry file. This is where we import babel polyfills and render the App into the root DOM node.                                                                                                   |
| `src/index.html` | The only HTML file in our App. All scripts and styles will be injected here by Webpack.                                                                                                              |
| `src/App`        | Main application routes, components that need to be mounted at all times regardless of current route, global css styles, fonts, etc. Basically anything considered global / ancestor of all modules. |
| `src/Auth`       | Authentication module                                                                                                                                                                                |
| `src/Project`    | Project module                                                                                                                                                                                       |
| `src/shared`     | Components, constants, utils, hooks, styles etc. that can be used anywhere in the codebase. Any module is allowed to import from shared.                                                             |


================================================
FILE: client/cypress/.eslintrc.json
================================================
{
  "extends": ["plugin:cypress/recommended"],
  "rules": {
    "no-unused-expressions": 0 // chai assertions trigger this rule
  }
}


================================================
FILE: client/cypress/integration/authentication.spec.js
================================================
import { testid } from '../support/utils';

describe('Authentication', () => {
  beforeEach(() => {
    cy.resetDatabase();
    cy.visit('/');
  });

  it('creates guest account if user has no auth token', () => {
    cy.window()
      .its('localStorage.authToken')
      .should('be.undefined');

    cy.window()
      .its('localStorage.authToken')
      .should('be.a', 'string')
      .and('not.be.empty');

    cy.get(testid`list-issue`).should('have.length', 8);
  });
});


================================================
FILE: client/cypress/integration/issueCreate.spec.js
================================================
import { testid } from '../support/utils';

describe('Issue create', () => {
  beforeEach(() => {
    cy.resetDatabase();
    cy.createTestAccount();
    cy.visit('/project/settings?modal-issue-create=true');
  });

  it('validates form and creates issue successfully', () => {
    cy.get(testid`modal:issue-create`).within(() => {
      cy.get('button[type="submit"]').click();
      cy.get(testid`form-field:title`).should('contain', 'This field is required');

      cy.selectOption('type', 'Story');
      cy.get('input[name="title"]').type('TEST_TITLE');
      cy.get('.ql-editor').type('TEST_DESCRIPTION');
      cy.selectOption('reporterId', 'Yoda');
      cy.selectOption('userIds', 'Gaben', 'Yoda');
      cy.selectOption('priority', 'High');

      cy.get('button[type="submit"]').click();
    });

    cy.get(testid`modal:issue-create`).should('not.exist');
    cy.contains('Issue has been successfully created.').should('exist');
    cy.location('pathname').should('equal', '/project/board');
    cy.location('search').should('be.empty');

    cy.contains(testid`list-issue`, 'TEST_TITLE')
      .should('have.descendants', testid`avatar:Gaben`)
      .and('have.descendants', testid`avatar:Yoda`)
      .and('have.descendants', testid`icon:story`);
  });
});


================================================
FILE: client/cypress/integration/issueDetails.spec.js
================================================
import { testid } from '../support/utils';

describe('Issue details', () => {
  beforeEach(() => {
    cy.resetDatabase();
    cy.createTestAccount();
    cy.visit('/project/board');
    getListIssue().click(); // open issue details modal
  });

  it('updates type, status, assignees, reporter, priority successfully', () => {
    getIssueDetailsModal().within(() => {
      cy.selectOption('type', 'Story');
      cy.selectOption('status', 'Done');
      cy.selectOption('assignees', 'Gaben', 'Yoda');
      cy.selectOption('reporter', 'Yoda');
      cy.selectOption('priority', 'Medium');
    });

    cy.assertReloadAssert(() => {
      getIssueDetailsModal().within(() => {
        cy.selectShouldContain('type', 'Story');
        cy.selectShouldContain('status', 'Done');
        cy.selectShouldContain('assignees', 'Gaben', 'Yoda');
        cy.selectShouldContain('reporter', 'Yoda');
        cy.selectShouldContain('priority', 'Medium');
      });

      getListIssue()
        .should('have.descendants', testid`avatar:Gaben`)
        .and('have.descendants', testid`avatar:Yoda`)
        .and('have.descendants', testid`icon:story`);
    });
  });

  it('updates title, description successfully', () => {
    getIssueDetailsModal().within(() => {
      cy.get('textarea[placeholder="Short summary"]')
        .clear()
        .type('TEST_TITLE')
        .blur();

      cy.contains('Add a description...')
        .click()
        .should('not.exist');

      cy.get('.ql-editor').type('TEST_DESCRIPTION');

      cy.contains('button', 'Save')
        .click()
        .should('not.exist');
    });

    cy.assertReloadAssert(() => {
      getIssueDetailsModal().within(() => {
        cy.get('textarea[placeholder="Short summary"]').should('have.value', 'TEST_TITLE');
        cy.get('.ql-editor').should('contain', 'TEST_DESCRIPTION');
      });

      cy.get(testid`list-issue`).should('contain', 'TEST_TITLE');
    });
  });

  it('updates estimate, time tracking successfully', () => {
    getIssueDetailsModal().within(() => {
      getNumberInputAtIndex(0).debounced('type', '10');
      cy.contains('10h estimated').click(); // open tracking modal
    });

    cy.get(testid`modal:tracking`).within(() => {
      cy.contains('No time logged').should('exist');

      getNumberInputAtIndex(0).debounced('type', 1);
      cy.get('div[width="10"]').should('exist'); // tracking bar

      getNumberInputAtIndex(1).debounced('type', 2);

      cy.contains('button', 'Done')
        .click()
        .should('not.exist');
    });

    cy.assertReloadAssert(() => {
      getIssueDetailsModal().within(() => {
        getNumberInputAtIndex(0).should('have.value', '10');
        cy.contains('1h logged').should('exist');
        cy.contains('2h remaining').should('exist');
        cy.get('div[width*="33.3333"]').should('exist');
      });
    });
  });

  it('deletes an issue successfully', () => {
    getIssueDetailsModal()
      .find(`button ${testid`icon:trash`}`)
      .click();

    cy.get(testid`modal:confirm`)
      .contains('button', 'Delete issue')
      .click();

    cy.assertReloadAssert(() => {
      getIssueDetailsModal().should('not.exist');
      getListIssue().should('not.exist');
    });
  });

  it('creates a comment successfully', () => {
    getIssueDetailsModal().within(() => {
      cy.contains('Add a comment...')
        .click()
        .should('not.exist');

      cy.get('textarea[placeholder="Add a comment..."]').type('TEST_COMMENT');

      cy.contains('button', 'Save')
        .click()
        .should('not.exist');

      cy.contains('Add a comment...').should('exist');
      cy.get(testid`issue-comment`).should('contain', 'TEST_COMMENT');
    });
  });

  it('edits a comment successfully', () => {
    getIssueDetailsModal().within(() => {
      cy.get(testid`issue-comment`)
        .contains('Edit')
        .click()
        .should('not.exist');

      cy.get('textarea[placeholder="Add a comment..."]')
        .should('have.value', 'Comment body')
        .clear()
        .type('TEST_COMMENT_EDITED');

      cy.contains('button', 'Save')
        .click()
        .should('not.exist');

      cy.get(testid`issue-comment`)
        .should('contain', 'Edit')
        .and('contain', 'TEST_COMMENT_EDITED');
    });
  });

  it('deletes a comment successfully', () => {
    getIssueDetailsModal()
      .find(testid`issue-comment`)
      .contains('Delete')
      .click();

    cy.get(testid`modal:confirm`)
      .contains('button', 'Delete comment')
      .click()
      .should('not.exist');

    getIssueDetailsModal()
      .find(testid`issue-comment`)
      .should('not.exist');
  });

  const getIssueDetailsModal = () => cy.get(testid`modal:issue-details`);
  const getListIssue = () => cy.contains(testid`list-issue`, 'Issue title 1');
  const getNumberInputAtIndex = index => cy.get('input[placeholder="Number"]').eq(index);
});


================================================
FILE: client/cypress/integration/issueFilters.spec.js
================================================
import { testid } from '../support/utils';

describe('Issue filters', () => {
  beforeEach(() => {
    cy.resetDatabase();
    cy.createTestAccount();
    cy.visit('/project/board');
  });

  it('filters issues', () => {
    getSearchInput().debounced('type', 'Issue title 1');
    assertIssuesCount(1);
    getSearchInput().debounced('clear');
    assertIssuesCount(3);

    getUserAvatar().click();
    assertIssuesCount(2);
    getUserAvatar().click();
    assertIssuesCount(3);

    getMyOnlyButton().click();
    assertIssuesCount(2);
    getMyOnlyButton().click();
    assertIssuesCount(3);

    getRecentButton().click();
    assertIssuesCount(3);
  });

  const getSearchInput = () => cy.get(testid`board-filters`).find('input');
  const getUserAvatar = () => cy.get(testid`board-filters`).find(testid`avatar:Gaben`);
  const getMyOnlyButton = () => cy.get(testid`board-filters`).contains('Only My Issues');
  const getRecentButton = () => cy.get(testid`board-filters`).contains('Recently Updated');
  const assertIssuesCount = count => cy.get(testid`list-issue`).should('have.length', count);
});


================================================
FILE: client/cypress/integration/issueSearch.spec.js
================================================
import { testid } from '../support/utils';

describe('Issue search', () => {
  beforeEach(() => {
    cy.resetDatabase();
    cy.createTestAccount();
    cy.visit('/project/board?modal-issue-search=true');
  });

  it('displays recent issues if search input is empty', () => {
    getIssueSearchModal().within(() => {
      cy.contains('Recent Issues').should('exist');
      getIssues().should('have.length', 3);

      cy.get('input').debounced('type', 'anything');
      cy.contains('Recent Issues').should('not.exist');

      cy.get('input').debounced('clear');
      cy.contains('Recent Issues').should('exist');
      getIssues().should('have.length', 3);
    });
  });

  it('displays matching issues successfully', () => {
    getIssueSearchModal().within(() => {
      cy.get('input').debounced('type', 'Issue');
      getIssues().should('have.length', 3);

      cy.get('input').debounced('type', ' description');
      getIssues().should('have.length', 2);

      cy.get('input').debounced('type', ' 3');
      getIssues().should('have.length', 1);

      cy.contains('Matching Issues').should('exist');
    });
  });

  it('displays message if no results were found', () => {
    getIssueSearchModal().within(() => {
      cy.get('input').debounced('type', 'gibberish');

      getIssues().should('not.exist');
      cy.contains("We couldn't find anything matching your search").should('exist');
    });
  });

  const getIssueSearchModal = () => cy.get(testid`modal:issue-search`);
  const getIssues = () => cy.get('a[href*="/project/board/issues/"]');
});


================================================
FILE: client/cypress/integration/issuesDragDrop.spec.js
================================================
import { KeyCodes } from 'shared/constants/keyCodes';

import { testid } from '../support/utils';

describe('Issues drag & drop', () => {
  beforeEach(() => {
    cy.resetDatabase();
    cy.createTestAccount();
    cy.visit('/project/board');
  });

  it('moves issue between different lists', () => {
    cy.get(testid`board-list:backlog`).should('contain', firstIssueTitle);
    cy.get(testid`board-list:selected`).should('not.contain', firstIssueTitle);
    moveFirstIssue(KeyCodes.ARROW_RIGHT);

    cy.assertReloadAssert(() => {
      cy.get(testid`board-list:backlog`).should('not.contain', firstIssueTitle);
      cy.get(testid`board-list:selected`).should('contain', firstIssueTitle);
    });
  });

  it('moves issue within a single list', () => {
    getIssueAtIndex(0).should('contain', firstIssueTitle);
    getIssueAtIndex(1).should('contain', secondIssueTitle);
    moveFirstIssue(KeyCodes.ARROW_DOWN);

    cy.assertReloadAssert(() => {
      getIssueAtIndex(0).should('contain', secondIssueTitle);
      getIssueAtIndex(1).should('contain', firstIssueTitle);
    });
  });

  const firstIssueTitle = 'Issue title 1';
  const secondIssueTitle = 'Issue title 2';

  const getIssueAtIndex = index => cy.get(testid`list-issue`).eq(index);

  const moveFirstIssue = directionKeyCode => {
    cy.waitForXHR('PUT', '/issues/**', () => {
      getIssueAtIndex(0)
        .focus()
        .trigger('keydown', { keyCode: KeyCodes.SPACE })
        .trigger('keydown', { keyCode: directionKeyCode, force: true })
        .trigger('keydown', { keyCode: KeyCodes.SPACE, force: true });
    });
  };
});


================================================
FILE: client/cypress/integration/projectSettings.spec.js
================================================
import { testid } from '../support/utils';

describe('Project settings', () => {
  beforeEach(() => {
    cy.resetDatabase();
    cy.createTestAccount();
    cy.visit('/project/settings');
  });

  it('should display current values in form', () => {
    cy.get('input[name="name"]').should('have.value', 'Project name');
    cy.get('input[name="url"]').should('have.value', 'https://www.testurl.com');
    cy.get('.ql-editor').should('contain', 'Project description');
    cy.selectShouldContain('category', 'Software');
  });

  it('validates form and updates project successfully', () => {
    cy.get('input[name="name"]').clear();
    cy.get('button[type="submit"]').click();
    cy.get(testid`form-field:name`).should('contain', 'This field is required');

    cy.get('input[name="name"]').type('TEST_NAME');
    cy.get(testid`form-field:name`).should('not.contain', 'This field is required');

    cy.selectOption('category', 'Business');
    cy.get('button[type="submit"]').click();
    cy.contains('Changes have been saved successfully.').should('exist');

    cy.reload();

    cy.get('input[name="name"]').should('have.value', 'TEST_NAME');
    cy.selectShouldContain('category', 'Business');
  });
});


================================================
FILE: client/cypress/plugins/index.js
================================================
/* eslint-disable global-require */
/* eslint-disable import/no-extraneous-dependencies */

// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************

// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)

const webpack = require('@cypress/webpack-preprocessor');
const webpackOptions = require('../../webpack.config.js');

module.exports = on => {
  on('file:preprocessor', webpack({ webpackOptions }));
};


================================================
FILE: client/cypress/support/commands.js
================================================
import 'core-js/stable';
import 'regenerator-runtime/runtime';

import '@4tw/cypress-drag-drop';

import { objectToQueryString } from 'shared/utils/url';
import { getStoredAuthToken, storeAuthToken } from 'shared/utils/authToken';

import { testid } from './utils';

Cypress.Commands.add('selectOption', (selectName, ...optionLabels) => {
  optionLabels.forEach(optionLabel => {
    cy.get(testid`select:${selectName}`).click('bottomRight');
    cy.get(testid`select-option:${optionLabel}`).click();
  });
});

Cypress.Commands.add('selectShouldContain', (selectName, ...optionLabels) => {
  optionLabels.forEach(optionLabel => {
    cy.get(testid`select:${selectName}`).should('contain', optionLabel);
  });
});

// We don't want to waste time when running tests on cypress waiting for debounced
// inputs. We can use tick() to speed up time and trigger onChange immediately.
Cypress.Commands.add('debounced', { prevSubject: true }, (input, action, value) => {
  cy.clock();
  cy.wrap(input)[action](value);
  cy.tick(1000);
});

// Sometimes cypress fails to properly wait for api requests to finish which results
// in flaky tests, and in those cases we need to explicitly tell it to wait
// https://docs.cypress.io/guides/guides/network-requests.html#Flake
Cypress.Commands.add('waitForXHR', (method, url, funcThatTriggersXHR) => {
  const alias = method + url;
  cy.server();
  cy.route(method, url).as(alias);
  funcThatTriggersXHR();
  cy.wait(`@${alias}`);
});

// We're using optimistic updates (not waiting for API response before updating
// the local data and UI) in a lot of places in the app. That's why we want to assert
// both the immediate local UI change in the first assert, and if the change was
// successfully persisted by the API in the second assert after page reload
Cypress.Commands.add('assertReloadAssert', assertFunc => {
  assertFunc();
  cy.reload();
  assertFunc();
});

Cypress.Commands.add('apiRequest', (method, url, variables = {}, options = {}) => {
  cy.request({
    method,
    url: `${Cypress.env('apiBaseUrl')}${url}`,
    qs: method === 'GET' ? objectToQueryString(variables) : undefined,
    body: method !== 'GET' ? variables : undefined,
    headers: {
      'Content-Type': 'application/json',
      Authorization: getStoredAuthToken() ? `Bearer ${getStoredAuthToken()}` : undefined,
    },
    ...options,
  });
});

Cypress.Commands.add('resetDatabase', () => {
  cy.apiRequest('DELETE', '/test/reset-database');
});

Cypress.Commands.add('createTestAccount', () => {
  cy.apiRequest('POST', '/test/create-account').then(response => {
    storeAuthToken(response.body.authToken);
  });
});


================================================
FILE: client/cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

import './commands';


================================================
FILE: client/cypress/support/utils.js
================================================
export const testid = (strings, ...values) => {
  const id = strings.map((str, index) => str + (values[index] || '')).join('');
  return `[data-testid="${id}"]`;
};


================================================
FILE: client/cypress.json
================================================
{
  "baseUrl": "http://localhost:8080",
  "viewportHeight": 800,
  "viewportWidth": 1440,
  "env": {
    "apiBaseUrl": "http://localhost:3000"
  }
}


================================================
FILE: client/jest/fileMock.js
================================================
module.exports = 'test-file-stub';


================================================
FILE: client/jest/styleMock.js
================================================
module.exports = {};


================================================
FILE: client/jest.config.js
================================================
module.exports = {
  moduleFileExtensions: ['*', 'js', 'jsx'],
  moduleDirectories: ['src', 'node_modules'],
  moduleNameMapper: {
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      '<rootDir>/jest/fileMock.js',
    '\\.(css|scss|less)$': '<rootDir>/jest/styleMock.js',
  },
};


================================================
FILE: client/jsconfig.json
================================================
// This config allows VSCode intellisense to work with absolute "src" imports and jsx files
{
  "compilerOptions": {
    "baseUrl": "./src",
    "jsx": "react"
  },
  "include": ["src/**/*", "cypress/**/*.js", "./node_modules/cypress"]
}


================================================
FILE: client/package.json
================================================
{
  "name": "jira_client",
  "version": "1.0.0",
  "author": "Ivor Reic",
  "license": "MIT",
  "scripts": {
    "start": "webpack-dev-server",
    "start:production": "pm2 start --name 'jira_client' server.js",
    "test:jest": "jest",
    "test:cypress": "node_modules/.bin/cypress open",
    "build": "rm -rf build && webpack --config webpack.config.production.js --progress",
    "pre-commit": "lint-staged"
  },
  "devDependencies": {
    "@babel/core": "^7.7.4",
    "@babel/plugin-proposal-class-properties": "^7.7.4",
    "@babel/plugin-proposal-decorators": "^7.7.4",
    "@babel/plugin-proposal-export-namespace-from": "^7.7.4",
    "@babel/plugin-syntax-dynamic-import": "^7.7.4",
    "@babel/preset-env": "^7.7.4",
    "@babel/preset-react": "^7.7.4",
    "@cypress/webpack-preprocessor": "^4.1.1",
    "babel-eslint": "^10.0.3",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.3.2",
    "cypress": "^3.8.1",
    "eslint": "^6.1.0",
    "eslint-config-airbnb": "^18.0.1",
    "eslint-config-prettier": "^6.7.0",
    "eslint-plugin-cypress": "^2.8.1",
    "eslint-plugin-import": "^2.18.2",
    "eslint-plugin-jsx-a11y": "^6.2.3",
    "eslint-plugin-react": "^7.17.0",
    "eslint-plugin-react-hooks": "^1.7.0",
    "file-loader": "^5.0.2",
    "html-webpack-plugin": "^3.2.0",
    "jest": "^24.9.0",
    "lint-staged": "^9.5.0",
    "prettier": "^1.19.1",
    "style-loader": "^1.0.1",
    "url-loader": "^3.0.0",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0"
  },
  "dependencies": {
    "@4tw/cypress-drag-drop": "^1.3.0",
    "axios": "^0.19.0",
    "color": "^3.1.2",
    "compression": "^1.7.4",
    "core-js": "^3.4.7",
    "express": "^4.17.1",
    "express-history-api-fallback": "^2.2.1",
    "formik": "^2.1.1",
    "history": "^4.10.1",
    "jwt-decode": "^2.2.0",
    "lodash": "^4.17.15",
    "moment": "^2.24.0",
    "prop-types": "^15.7.2",
    "query-string": "^6.9.0",
    "quill": "^1.3.7",
    "react": "^16.12.0",
    "react-beautiful-dnd": "^12.2.0",
    "react-content-loader": "^4.3.3",
    "react-dom": "^16.12.0",
    "react-router-dom": "^5.1.2",
    "react-textarea-autosize": "^7.1.2",
    "react-transition-group": "^4.3.0",
    "regenerator-runtime": "^0.13.3",
    "styled-components": "^4.4.1",
    "sweet-pubsub": "^1.1.2"
  },
  "lint-staged": {
    "*.{js,jsx}": [
      "eslint --fix",
      "prettier --write",
      "git add"
    ]
  }
}


================================================
FILE: client/server.js
================================================
const express = require('express');
const fallback = require('express-history-api-fallback');
const compression = require('compression');

const app = express();

app.use(compression());

app.use(express.static(`${__dirname}/build`));

app.use(fallback(`${__dirname}/build/index.html`));

app.listen(process.env.PORT || 8081);


================================================
FILE: client/src/App/BaseStyles.js
================================================
import { createGlobalStyle } from 'styled-components';

import { color, font, mixin } from 'shared/utils/styles';

export default createGlobalStyle`
  html, body, #root {
    height: 100%;
    min-height: 100%;
    min-width: 768px;
  }

  body {
    color: ${color.textDarkest};
    -webkit-tap-highlight-color: transparent;
    line-height: 1.2;
    ${font.size(16)}
    ${font.regular}
  }

  #root {
    display: flex;
    flex-direction: column;
  }

  button,
  input,
  optgroup,
  select,
  textarea {
    ${font.regular}
  }

  *, *:after, *:before, input[type="search"] {
    box-sizing: border-box;
  }

  a {
    color: inherit;
    text-decoration: none;
  }

  ul {
    list-style: none;
  }

  ul, li, ol, dd, h1, h2, h3, h4, h5, h6, p {
    padding: 0;
    margin: 0;
  }

  h1, h2, h3, h4, h5, h6, strong {
    ${font.bold}
  }

  button {
    background: none;
    border: none;
  }

  /* Workaround for IE11 focus highlighting for select elements */
  select::-ms-value {
    background: none;
    color: #42413d;
  }

  [role="button"], button, input, select, textarea {
    outline: none;
    &:focus {
      outline: none;
    }
    &:disabled {
      opacity: 1;
    }
  }
  [role="button"], button, input, textarea {
    appearance: none;
  }
  select:-moz-focusring {
    color: transparent;
    text-shadow: 0 0 0 #000;
  }
  select::-ms-expand {
    display: none;
  }
  select option {
    color: ${color.textDarkest};
  }

  p {
    line-height: 1.4285;
    a {
      ${mixin.link()}
    }
  }

  textarea {
    line-height: 1.4285;
  }

  body, select {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

  html {
    touch-action: manipulation;
  }

  ${mixin.placeholderColor(color.textLight)}
`;


================================================
FILE: client/src/App/NormalizeStyles.js
================================================
import { createGlobalStyle } from 'styled-components';

/** DO NOT ALTER THIS FILE. It is a copy of https://necolas.github.io/normalize.css/ */

export default createGlobalStyle`
  html {
    line-height: 1.15;
    -webkit-text-size-adjust: 100%;
  }
  body {
    margin: 0;
  }
  main {
    display: block;
  }
  h1 {
    font-size: 2em;
    margin: 0.67em 0;
  }
  hr {
    box-sizing: content-box;
    height: 0;
    overflow: visible;
  }
  pre {
    font-family: monospace, monospace;
    font-size: 1em;
  }
  a {
    background-color: transparent;
  }
  abbr[title] {
    border-bottom: none;
    text-decoration: underline;
    text-decoration: underline dotted;
  }
  b,
  strong {
    font-weight: bolder;
  }
  code,
  kbd,
  samp {
    font-family: monospace, monospace;
    font-size: 1em;
  }
  small {
    font-size: 80%;
  }
  sub,
  sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
  }
  sub {
    bottom: -0.25em;
  }
  sup {
    top: -0.5em;
  }
  img {
    border-style: none;
  }
  button,
  input,
  optgroup,
  select,
  textarea {
    font-family: inherit;
    font-size: 100%;
    line-height: 1.15;
    margin: 0;
  }
  button,
  input {
    overflow: visible;
  }
  button,
  select {
    text-transform: none;
  }
  button,
  [type="button"],
  [type="reset"],
  [type="submit"] {
    -webkit-appearance: button;
  }
  button::-moz-focus-inner,
  [type="button"]::-moz-focus-inner,
  [type="reset"]::-moz-focus-inner,
  [type="submit"]::-moz-focus-inner {
    border-style: none;
    padding: 0;
  }
  button:-moz-focusring,
  [type="button"]:-moz-focusring,
  [type="reset"]:-moz-focusring,
  [type="submit"]:-moz-focusring {
    outline: 1px dotted ButtonText;
  }
  fieldset {
    padding: 0.35em 0.75em 0.625em;
  }
  legend {
    box-sizing: border-box;
    color: inherit;
    display: table;
    max-width: 100%;
    padding: 0;
    white-space: normal;
  }
  progress {
    vertical-align: baseline;
  }
  textarea {
    overflow: auto;
  }
  [type="checkbox"],
  [type="radio"] {
    box-sizing: border-box;
    padding: 0;
  }
  [type="number"]::-webkit-inner-spin-button,
  [type="number"]::-webkit-outer-spin-button {
    height: auto;
  }
  [type="search"] {
    -webkit-appearance: textfield;
    outline-offset: -2px;
  }
  [type="search"]::-webkit-search-decoration {
    -webkit-appearance: none;
  }
  ::-webkit-file-upload-button {
    -webkit-appearance: button;
    font: inherit;
  }
  details {
    display: block;
  }
  summary {
    display: list-item;
  }
  template {
    display: none;
  }
  [hidden] {
    display: none;
  }
`;


================================================
FILE: client/src/App/Routes.jsx
================================================
import React from 'react';
import { Router, Switch, Route, Redirect } from 'react-router-dom';

import history from 'browserHistory';
import Project from 'Project';
import Authenticate from 'Auth/Authenticate';
import PageError from 'shared/components/PageError';

const Routes = () => (
  <Router history={history}>
    <Switch>
      <Redirect exact from="/" to="/project" />
      <Route path="/authenticate" component={Authenticate} />
      <Route path="/project" component={Project} />
      <Route component={PageError} />
    </Switch>
  </Router>
);

export default Routes;


================================================
FILE: client/src/App/Toast/Styles.js
================================================
import styled from 'styled-components';

import { color, font, mixin, zIndexValues } from 'shared/utils/styles';
import { Icon } from 'shared/components';

export const Container = styled.div`
  z-index: ${zIndexValues.modal + 1};
  position: fixed;
  right: 30px;
  top: 50px;
`;

export const StyledToast = styled.div`
  position: relative;
  margin-bottom: 5px;
  width: 300px;
  padding: 15px 20px;
  border-radius: 3px;
  color: #fff;
  background: ${props => color[props.type]};
  cursor: pointer;
  transition: all 0.15s;
  ${mixin.clearfix}
  ${mixin.hardwareAccelerate}

  &.jira-toast-enter,
  &.jira-toast-exit.jira-toast-exit-active {
    opacity: 0;
    right: -10px;
  }

  &.jira-toast-exit,
  &.jira-toast-enter.jira-toast-enter-active {
    opacity: 1;
    right: 0;
  }
`;

export const CloseIcon = styled(Icon)`
  position: absolute;
  top: 13px;
  right: 14px;
  font-size: 22px;
  cursor: pointer;
  color: #fff;
`;

export const Title = styled.div`
  padding-right: 22px;
  ${font.size(15)}
  ${font.medium}
`;

export const Message = styled.div`
  padding: 8px 10px 0 0;
  white-space: pre-wrap;
  ${font.size(14)}
  ${font.medium}
`;


================================================
FILE: client/src/App/Toast/index.jsx
================================================
import React, { useState, useEffect } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import pubsub from 'sweet-pubsub';
import { uniqueId } from 'lodash';

import { Container, StyledToast, CloseIcon, Title, Message } from './Styles';

const Toast = () => {
  const [toasts, setToasts] = useState([]);

  useEffect(() => {
    const addToast = ({ type = 'success', title, message, duration = 5 }) => {
      const id = uniqueId('toast-');

      setToasts(currentToasts => [...currentToasts, { id, type, title, message }]);

      if (duration) {
        setTimeout(() => removeToast(id), duration * 1000);
      }
    };

    pubsub.on('toast', addToast);

    return () => {
      pubsub.off('toast', addToast);
    };
  }, []);

  const removeToast = id => {
    setToasts(currentToasts => currentToasts.filter(toast => toast.id !== id));
  };

  return (
    <Container>
      <TransitionGroup>
        {toasts.map(toast => (
          <CSSTransition key={toast.id} classNames="jira-toast" timeout={200}>
            <StyledToast key={toast.id} type={toast.type} onClick={() => removeToast(toast.id)}>
              <CloseIcon type="close" />
              {toast.title && <Title>{toast.title}</Title>}
              {toast.message && <Message>{toast.message}</Message>}
            </StyledToast>
          </CSSTransition>
        ))}
      </TransitionGroup>
    </Container>
  );
};

export default Toast;


================================================
FILE: client/src/App/fontStyles.css
================================================
@font-face {
  font-family: 'CircularStdBlack';
  src: url('./assets/fonts/CircularStd-Black.woff2') format('woff2'),
    url('./assets/fonts/CircularStd-Black.woff') format('woff');
  font-weight: normal;
  font-style: normal;
}
@font-face {
  font-family: 'CircularStdBold';
  src: url('./assets/fonts/CircularStd-Bold.woff2') format('woff2'),
    url('./assets/fonts/CircularStd-Bold.woff') format('woff');
  font-weight: normal;
  font-style: normal;
}
@font-face {
  font-family: 'CircularStdMedium';
  src: url('./assets/fonts/CircularStd-Medium.woff2') format('woff2'),
    url('./assets/fonts/CircularStd-Medium.woff') format('woff');
  font-weight: normal;
  font-style: normal;
}
@font-face {
  font-family: 'CircularStdBook';
  src: url('./assets/fonts/CircularStd-Book.woff2') format('woff2'),
    url('./assets/fonts/CircularStd-Book.woff') format('woff');
  font-weight: normal;
  font-style: normal;
}
@font-face {
  font-family: 'jira';
  src: url('./assets/fonts/jira.woff') format('truetype'),
    url('./assets/fonts/jira.ttf') format('woff'), url('./assets/fonts/jira.svg#jira') format('svg');
  font-weight: normal;
  font-style: normal;
}


================================================
FILE: client/src/App/index.jsx
================================================
import React, { Fragment } from 'react';

import NormalizeStyles from './NormalizeStyles';
import BaseStyles from './BaseStyles';
import Toast from './Toast';
import Routes from './Routes';

// We're importing .css because @font-face in styled-components causes font files
// to be constantly re-requested from the server (which causes screen flicker)
// https://github.com/styled-components/styled-components/issues/1593
import './fontStyles.css';

const App = () => (
  <Fragment>
    <NormalizeStyles />
    <BaseStyles />
    <Toast />
    <Routes />
  </Fragment>
);

export default App;


================================================
FILE: client/src/Auth/Authenticate.jsx
================================================
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';

import api from 'shared/utils/api';
import toast from 'shared/utils/toast';
import { getStoredAuthToken, storeAuthToken } from 'shared/utils/authToken';
import { PageLoader } from 'shared/components';

const Authenticate = () => {
  const history = useHistory();

  useEffect(() => {
    const createGuestAccount = async () => {
      try {
        const { authToken } = await api.post('/authentication/guest');
        storeAuthToken(authToken);
        history.push('/');
      } catch (error) {
        toast.error(error);
      }
    };

    if (!getStoredAuthToken()) {
      createGuestAccount();
    }
  }, [history]);

  return <PageLoader />;
};

export default Authenticate;


================================================
FILE: client/src/Project/Board/Filters/Styles.js
================================================
import styled from 'styled-components';

import { color, font, mixin } from 'shared/utils/styles';
import { InputDebounced, Avatar, Button } from 'shared/components';

export const Filters = styled.div`
  display: flex;
  align-items: center;
  margin-top: 24px;
`;

export const SearchInput = styled(InputDebounced)`
  margin-right: 18px;
  width: 160px;
`;

export const Avatars = styled.div`
  display: flex;
  flex-direction: row-reverse;
  margin: 0 12px 0 2px;
`;

export const AvatarIsActiveBorder = styled.div`
  display: inline-flex;
  margin-left: -2px;
  border-radius: 50%;
  transition: transform 0.1s;
  ${mixin.clickable};
  ${props => props.isActive && `box-shadow: 0 0 0 4px ${color.primary}`}
  &:hover {
    transform: translateY(-5px);
  }
`;

export const StyledAvatar = styled(Avatar)`
  box-shadow: 0 0 0 2px #fff;
`;

export const StyledButton = styled(Button)`
  margin-left: 6px;
`;

export const ClearAll = styled.div`
  height: 32px;
  line-height: 32px;
  margin-left: 15px;
  padding-left: 12px;
  border-left: 1px solid ${color.borderLightest};
  color: ${color.textDark};
  ${font.size(14.5)}
  ${mixin.clickable}
  &:hover {
    color: ${color.textMedium};
  }
`;


================================================
FILE: client/src/Project/Board/Filters/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { xor } from 'lodash';

import {
  Filters,
  SearchInput,
  Avatars,
  AvatarIsActiveBorder,
  StyledAvatar,
  StyledButton,
  ClearAll,
} from './Styles';

const propTypes = {
  projectUsers: PropTypes.array.isRequired,
  defaultFilters: PropTypes.object.isRequired,
  filters: PropTypes.object.isRequired,
  mergeFilters: PropTypes.func.isRequired,
};

const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, mergeFilters }) => {
  const { searchTerm, userIds, myOnly, recent } = filters;

  const areFiltersCleared = !searchTerm && userIds.length === 0 && !myOnly && !recent;

  return (
    <Filters data-testid="board-filters">
      <SearchInput
        icon="search"
        value={searchTerm}
        onChange={value => mergeFilters({ searchTerm: value })}
      />
      <Avatars>
        {projectUsers.map(user => (
          <AvatarIsActiveBorder key={user.id} isActive={userIds.includes(user.id)}>
            <StyledAvatar
              avatarUrl={user.avatarUrl}
              name={user.name}
              onClick={() => mergeFilters({ userIds: xor(userIds, [user.id]) })}
            />
          </AvatarIsActiveBorder>
        ))}
      </Avatars>
      <StyledButton
        variant="empty"
        isActive={myOnly}
        onClick={() => mergeFilters({ myOnly: !myOnly })}
      >
        Only My Issues
      </StyledButton>
      <StyledButton
        variant="empty"
        isActive={recent}
        onClick={() => mergeFilters({ recent: !recent })}
      >
        Recently Updated
      </StyledButton>
      {!areFiltersCleared && (
        <ClearAll onClick={() => mergeFilters(defaultFilters)}>Clear all</ClearAll>
      )}
    </Filters>
  );
};

ProjectBoardFilters.propTypes = propTypes;

export default ProjectBoardFilters;


================================================
FILE: client/src/Project/Board/Header/Styles.js
================================================
import styled from 'styled-components';

import { font } from 'shared/utils/styles';

export const Header = styled.div`
  margin-top: 6px;
  display: flex;
  justify-content: space-between;
`;

export const BoardName = styled.div`
  ${font.size(24)}
  ${font.medium}
`;


================================================
FILE: client/src/Project/Board/Header/index.jsx
================================================
import React from 'react';

import { Button } from 'shared/components';

import { Header, BoardName } from './Styles';

const ProjectBoardHeader = () => (
  <Header>
    <BoardName>Kanban board</BoardName>
    <a href="https://github.com/oldboyxx/jira_clone" target="_blank" rel="noreferrer noopener">
      <Button icon="github">Github Repo</Button>
    </a>
  </Header>
);

export default ProjectBoardHeader;


================================================
FILE: client/src/Project/Board/IssueDetails/AssigneesReporter/Styles.js
================================================
import styled, { css } from 'styled-components';

import { color, font, mixin } from 'shared/utils/styles';

export const User = styled.div`
  display: flex;
  align-items: center;
  ${mixin.clickable}
  ${props =>
    props.isSelectValue &&
    css`
      margin: 0 10px ${props.withBottomMargin ? 5 : 0}px 0;
      padding: 4px 8px;
      border-radius: 4px;
      background: ${color.backgroundLight};
      transition: background 0.1s;
      &:hover {
        background: ${color.backgroundMedium};
      }
    `}
`;

export const Username = styled.div`
  padding: 0 3px 0 8px;
  ${font.size(14.5)}
`;


================================================
FILE: client/src/Project/Board/IssueDetails/AssigneesReporter/index.jsx
================================================
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';

import { Avatar, Select, Icon } from 'shared/components';

import { SectionTitle } from '../Styles';
import { User, Username } from './Styles';

const propTypes = {
  issue: PropTypes.object.isRequired,
  updateIssue: PropTypes.func.isRequired,
  projectUsers: PropTypes.array.isRequired,
};

const ProjectBoardIssueDetailsAssigneesReporter = ({ issue, updateIssue, projectUsers }) => {
  const getUserById = userId => projectUsers.find(user => user.id === userId);

  const userOptions = projectUsers.map(user => ({ value: user.id, label: user.name }));

  return (
    <Fragment>
      <SectionTitle>Assignees</SectionTitle>
      <Select
        isMulti
        variant="empty"
        dropdownWidth={343}
        placeholder="Unassigned"
        name="assignees"
        value={issue.userIds}
        options={userOptions}
        onChange={userIds => {
          updateIssue({ userIds, users: userIds.map(getUserById) });
        }}
        renderValue={({ value: userId, removeOptionValue }) =>
          renderUser(getUserById(userId), true, removeOptionValue)
        }
        renderOption={({ value: userId }) => renderUser(getUserById(userId), false)}
      />

      <SectionTitle>Reporter</SectionTitle>
      <Select
        variant="empty"
        dropdownWidth={343}
        withClearValue={false}
        name="reporter"
        value={issue.reporterId}
        options={userOptions}
        onChange={userId => updateIssue({ reporterId: userId })}
        renderValue={({ value: userId }) => renderUser(getUserById(userId), true)}
        renderOption={({ value: userId }) => renderUser(getUserById(userId))}
      />
    </Fragment>
  );
};

const renderUser = (user, isSelectValue, removeOptionValue) => (
  <User
    key={user.id}
    isSelectValue={isSelectValue}
    withBottomMargin={!!removeOptionValue}
    onClick={() => removeOptionValue && removeOptionValue()}
  >
    <Avatar avatarUrl={user.avatarUrl} name={user.name} size={24} />
    <Username>{user.name}</Username>
    {removeOptionValue && <Icon type="close" top={1} />}
  </User>
);

ProjectBoardIssueDetailsAssigneesReporter.propTypes = propTypes;

export default ProjectBoardIssueDetailsAssigneesReporter;


================================================
FILE: client/src/Project/Board/IssueDetails/Comments/BodyForm/Styles.js
================================================
import styled from 'styled-components';

import { Button } from 'shared/components';

export const Actions = styled.div`
  display: flex;
  padding-top: 10px;
`;

export const FormButton = styled(Button)`
  margin-right: 6px;
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Comments/BodyForm/index.jsx
================================================
import React, { Fragment, useRef } from 'react';
import PropTypes from 'prop-types';

import { Textarea } from 'shared/components';

import { Actions, FormButton } from './Styles';

const propTypes = {
  value: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
  isWorking: PropTypes.bool.isRequired,
  onSubmit: PropTypes.func.isRequired,
  onCancel: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsCommentsBodyForm = ({
  value,
  onChange,
  isWorking,
  onSubmit,
  onCancel,
}) => {
  const $textareaRef = useRef();

  const handleSubmit = () => {
    if ($textareaRef.current.value.trim()) {
      onSubmit();
    }
  };

  return (
    <Fragment>
      <Textarea
        autoFocus
        placeholder="Add a comment..."
        value={value}
        onChange={onChange}
        ref={$textareaRef}
      />
      <Actions>
        <FormButton variant="primary" isWorking={isWorking} onClick={handleSubmit}>
          Save
        </FormButton>
        <FormButton variant="empty" onClick={onCancel}>
          Cancel
        </FormButton>
      </Actions>
    </Fragment>
  );
};

ProjectBoardIssueDetailsCommentsBodyForm.propTypes = propTypes;

export default ProjectBoardIssueDetailsCommentsBodyForm;


================================================
FILE: client/src/Project/Board/IssueDetails/Comments/Comment/Styles.js
================================================
import styled, { css } from 'styled-components';

import { color, font, mixin } from 'shared/utils/styles';
import { Avatar } from 'shared/components';

export const Comment = styled.div`
  position: relative;
  margin-top: 25px;
  ${font.size(15)}
`;

export const UserAvatar = styled(Avatar)`
  position: absolute;
  top: 0;
  left: 0;
`;

export const Content = styled.div`
  padding-left: 44px;
`;

export const Username = styled.div`
  display: inline-block;
  padding-right: 12px;
  padding-bottom: 10px;
  color: ${color.textDark};
  ${font.medium}
`;

export const CreatedAt = styled.div`
  display: inline-block;
  padding-bottom: 10px;
  color: ${color.textDark};
  ${font.size(14.5)}
`;

export const Body = styled.p`
  padding-bottom: 10px;
  white-space: pre-wrap;
`;

const actionLinkStyles = css`
  display: inline-block;
  padding: 2px 0;
  color: ${color.textMedium};
  ${font.size(14.5)}
  ${mixin.clickable}
  &:hover {
    text-decoration: underline;
  }
`;

export const EditLink = styled.div`
  margin-right: 12px;
  ${actionLinkStyles}
`;

export const DeleteLink = styled.div`
  ${actionLinkStyles}
  &:before {
    position: relative;
    right: 6px;
    content: '·';
    display: inline-block;
  }
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Comments/Comment/index.jsx
================================================
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';

import api from 'shared/utils/api';
import toast from 'shared/utils/toast';
import { formatDateTimeConversational } from 'shared/utils/dateTime';
import { ConfirmModal } from 'shared/components';

import BodyForm from '../BodyForm';
import {
  Comment,
  UserAvatar,
  Content,
  Username,
  CreatedAt,
  Body,
  EditLink,
  DeleteLink,
} from './Styles';

const propTypes = {
  comment: PropTypes.object.isRequired,
  fetchIssue: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {
  const [isFormOpen, setFormOpen] = useState(false);
  const [isUpdating, setUpdating] = useState(false);
  const [body, setBody] = useState(comment.body);

  const handleCommentDelete = async () => {
    try {
      await api.delete(`/comments/${comment.id}`);
      await fetchIssue();
    } catch (error) {
      toast.error(error);
    }
  };

  const handleCommentUpdate = async () => {
    try {
      setUpdating(true);
      await api.put(`/comments/${comment.id}`, { body });
      await fetchIssue();
      setUpdating(false);
      setFormOpen(false);
    } catch (error) {
      toast.error(error);
    }
  };

  return (
    <Comment data-testid="issue-comment">
      <UserAvatar name={comment.user.name} avatarUrl={comment.user.avatarUrl} />
      <Content>
        <Username>{comment.user.name}</Username>
        <CreatedAt>{formatDateTimeConversational(comment.createdAt)}</CreatedAt>

        {isFormOpen ? (
          <BodyForm
            value={body}
            onChange={setBody}
            isWorking={isUpdating}
            onSubmit={handleCommentUpdate}
            onCancel={() => setFormOpen(false)}
          />
        ) : (
          <Fragment>
            <Body>{comment.body}</Body>
            <EditLink onClick={() => setFormOpen(true)}>Edit</EditLink>
            <ConfirmModal
              title="Are you sure you want to delete this comment?"
              message="Once you delete, it's gone for good."
              confirmText="Delete comment"
              onConfirm={handleCommentDelete}
              renderLink={modal => <DeleteLink onClick={modal.open}>Delete</DeleteLink>}
            />
          </Fragment>
        )}
      </Content>
    </Comment>
  );
};

ProjectBoardIssueDetailsComment.propTypes = propTypes;

export default ProjectBoardIssueDetailsComment;


================================================
FILE: client/src/Project/Board/IssueDetails/Comments/Create/ProTip/Styles.js
================================================
import styled from 'styled-components';

import { color, font } from 'shared/utils/styles';

export const Tip = styled.div`
  display: flex;
  align-items: center;
  padding-top: 8px;
  color: ${color.textMedium};
  ${font.size(13)}
  strong {
    padding-right: 4px;
  }
`;

export const TipLetter = styled.span`
  position: relative;
  top: 1px;
  display: inline-block;
  margin: 0 4px;
  padding: 0 4px;
  border-radius: 2px;
  color: ${color.textDarkest};
  background: ${color.backgroundMedium};
  ${font.bold}
  ${font.size(12)}
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Comments/Create/ProTip/index.jsx
================================================
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';

import { KeyCodes } from 'shared/constants/keyCodes';
import { isFocusedElementEditable } from 'shared/utils/browser';

import { Tip, TipLetter } from './Styles';

const propTypes = {
  setFormOpen: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsCommentsCreateProTip = ({ setFormOpen }) => {
  useEffect(() => {
    const handleKeyDown = event => {
      if (!isFocusedElementEditable() && event.keyCode === KeyCodes.M) {
        event.preventDefault();
        setFormOpen(true);
      }
    };

    document.addEventListener('keydown', handleKeyDown);

    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [setFormOpen]);

  return (
    <Tip>
      <strong>Pro tip:</strong>press<TipLetter>M</TipLetter>to comment
    </Tip>
  );
};

ProjectBoardIssueDetailsCommentsCreateProTip.propTypes = propTypes;

export default ProjectBoardIssueDetailsCommentsCreateProTip;


================================================
FILE: client/src/Project/Board/IssueDetails/Comments/Create/Styles.js
================================================
import styled from 'styled-components';

import { color, font, mixin } from 'shared/utils/styles';
import { Avatar } from 'shared/components';

export const Create = styled.div`
  position: relative;
  margin-top: 25px;
  ${font.size(15)}
`;

export const UserAvatar = styled(Avatar)`
  position: absolute;
  top: 0;
  left: 0;
`;

export const Right = styled.div`
  padding-left: 44px;
`;

export const FakeTextarea = styled.div`
  padding: 12px 16px;
  border-radius: 4px;
  border: 1px solid ${color.borderLightest};
  color: ${color.textLight};
  ${mixin.clickable}
  &:hover {
    border: 1px solid ${color.borderLight};
  }
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Comments/Create/index.jsx
================================================
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';

import api from 'shared/utils/api';
import useCurrentUser from 'shared/hooks/currentUser';
import toast from 'shared/utils/toast';

import BodyForm from '../BodyForm';
import ProTip from './ProTip';
import { Create, UserAvatar, Right, FakeTextarea } from './Styles';

const propTypes = {
  issueId: PropTypes.number.isRequired,
  fetchIssue: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsCommentsCreate = ({ issueId, fetchIssue }) => {
  const [isFormOpen, setFormOpen] = useState(false);
  const [isCreating, setCreating] = useState(false);
  const [body, setBody] = useState('');

  const { currentUser } = useCurrentUser();

  const handleCommentCreate = async () => {
    try {
      setCreating(true);
      await api.post(`/comments`, { body, issueId, userId: currentUser.id });
      await fetchIssue();
      setFormOpen(false);
      setCreating(false);
      setBody('');
    } catch (error) {
      toast.error(error);
    }
  };

  return (
    <Create>
      {currentUser && <UserAvatar name={currentUser.name} avatarUrl={currentUser.avatarUrl} />}
      <Right>
        {isFormOpen ? (
          <BodyForm
            value={body}
            onChange={setBody}
            isWorking={isCreating}
            onSubmit={handleCommentCreate}
            onCancel={() => setFormOpen(false)}
          />
        ) : (
          <Fragment>
            <FakeTextarea onClick={() => setFormOpen(true)}>Add a comment...</FakeTextarea>
            <ProTip setFormOpen={setFormOpen} />
          </Fragment>
        )}
      </Right>
    </Create>
  );
};

ProjectBoardIssueDetailsCommentsCreate.propTypes = propTypes;

export default ProjectBoardIssueDetailsCommentsCreate;


================================================
FILE: client/src/Project/Board/IssueDetails/Comments/Styles.js
================================================
import styled from 'styled-components';

import { font } from 'shared/utils/styles';

export const Comments = styled.div`
  padding-top: 40px;
`;

export const Title = styled.div`
  ${font.medium}
  ${font.size(15)}
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Comments/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';

import { sortByNewest } from 'shared/utils/javascript';

import Create from './Create';
import Comment from './Comment';
import { Comments, Title } from './Styles';

const propTypes = {
  issue: PropTypes.object.isRequired,
  fetchIssue: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsComments = ({ issue, fetchIssue }) => (
  <Comments>
    <Title>Comments</Title>
    <Create issueId={issue.id} fetchIssue={fetchIssue} />

    {sortByNewest(issue.comments, 'createdAt').map(comment => (
      <Comment key={comment.id} comment={comment} fetchIssue={fetchIssue} />
    ))}
  </Comments>
);

ProjectBoardIssueDetailsComments.propTypes = propTypes;

export default ProjectBoardIssueDetailsComments;


================================================
FILE: client/src/Project/Board/IssueDetails/Dates/Styles.js
================================================
import styled from 'styled-components';

import { color, font } from 'shared/utils/styles';

export const Dates = styled.div`
  margin-top: 11px;
  padding-top: 13px;
  line-height: 22px;
  border-top: 1px solid ${color.borderLightest};
  color: ${color.textMedium};
  ${font.size(13)}
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Dates/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';

import { formatDateTimeConversational } from 'shared/utils/dateTime';

import { Dates } from './Styles';

const propTypes = {
  issue: PropTypes.object.isRequired,
};

const ProjectBoardIssueDetailsDates = ({ issue }) => (
  <Dates>
    <div>Created at {formatDateTimeConversational(issue.createdAt)}</div>
    <div>Updated at {formatDateTimeConversational(issue.updatedAt)}</div>
  </Dates>
);

ProjectBoardIssueDetailsDates.propTypes = propTypes;

export default ProjectBoardIssueDetailsDates;


================================================
FILE: client/src/Project/Board/IssueDetails/Delete.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';

import api from 'shared/utils/api';
import toast from 'shared/utils/toast';
import { Button, ConfirmModal } from 'shared/components';

const propTypes = {
  issue: PropTypes.object.isRequired,
  fetchProject: PropTypes.func.isRequired,
  modalClose: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsDelete = ({ issue, fetchProject, modalClose }) => {
  const handleIssueDelete = async () => {
    try {
      await api.delete(`/issues/${issue.id}`);
      await fetchProject();
      modalClose();
    } catch (error) {
      toast.error(error);
    }
  };

  return (
    <ConfirmModal
      title="Are you sure you want to delete this issue?"
      message="Once you delete, it's gone for good."
      confirmText="Delete issue"
      onConfirm={handleIssueDelete}
      renderLink={modal => (
        <Button icon="trash" iconSize={19} variant="empty" onClick={modal.open} />
      )}
    />
  );
};

ProjectBoardIssueDetailsDelete.propTypes = propTypes;

export default ProjectBoardIssueDetailsDelete;


================================================
FILE: client/src/Project/Board/IssueDetails/Description/Styles.js
================================================
import styled from 'styled-components';

import { color, font, mixin } from 'shared/utils/styles';

export const Title = styled.div`
  padding: 20px 0 6px;
  ${font.size(15)}
  ${font.medium}
`;

export const EmptyLabel = styled.div`
  margin-left: -7px;
  padding: 7px;
  border-radius: 3px;
  color: ${color.textMedium}
  transition: background 0.1s;
  ${font.size(15)}
  ${mixin.clickable}
  &:hover {
    background: ${color.backgroundLight};
  }
`;

export const Actions = styled.div`
  display: flex;
  padding-top: 12px;
  & > button {
    margin-right: 6px;
  }
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Description/index.jsx
================================================
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';

import { getTextContentsFromHtmlString } from 'shared/utils/browser';
import { TextEditor, TextEditedContent, Button } from 'shared/components';

import { Title, EmptyLabel, Actions } from './Styles';

const propTypes = {
  issue: PropTypes.object.isRequired,
  updateIssue: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
  const [description, setDescription] = useState(issue.description);
  const [isEditing, setEditing] = useState(false);

  const handleUpdate = () => {
    setEditing(false);
    updateIssue({ description });
  };

  const isDescriptionEmpty = getTextContentsFromHtmlString(description).trim().length === 0;

  return (
    <Fragment>
      <Title>Description</Title>
      {isEditing ? (
        <Fragment>
          <TextEditor
            placeholder="Describe the issue"
            defaultValue={description}
            onChange={setDescription}
          />
          <Actions>
            <Button variant="primary" onClick={handleUpdate}>
              Save
            </Button>
            <Button variant="empty" onClick={() => setEditing(false)}>
              Cancel
            </Button>
          </Actions>
        </Fragment>
      ) : (
        <Fragment>
          {isDescriptionEmpty ? (
            <EmptyLabel onClick={() => setEditing(true)}>Add a description...</EmptyLabel>
          ) : (
            <TextEditedContent content={description} onClick={() => setEditing(true)} />
          )}
        </Fragment>
      )}
    </Fragment>
  );
};

ProjectBoardIssueDetailsDescription.propTypes = propTypes;

export default ProjectBoardIssueDetailsDescription;


================================================
FILE: client/src/Project/Board/IssueDetails/EstimateTracking/Styles.js
================================================
import styled from 'styled-components';

import { color, font, mixin } from 'shared/utils/styles';

export const TrackingLink = styled.div`
  padding: 4px 4px 2px 0;
  border-radius: 4px;
  transition: background 0.1s;
  ${mixin.clickable}
  &:hover {
    background: ${color.backgroundLight};
  }
`;

export const ModalContents = styled.div`
  padding: 20px 25px 25px;
`;

export const ModalTitle = styled.div`
  padding-bottom: 14px;
  ${font.medium}
  ${font.size(20)}
`;

export const Inputs = styled.div`
  display: flex;
  margin: 20px -5px 30px;
`;

export const InputCont = styled.div`
  margin: 0 5px;
  width: 50%;
`;

export const InputLabel = styled.div`
  padding-bottom: 5px;
  color: ${color.textMedium};
  ${font.medium};
  ${font.size(13)};
`;

export const Actions = styled.div`
  display: flex;
  justify-content: flex-end;
`;


================================================
FILE: client/src/Project/Board/IssueDetails/EstimateTracking/TrackingWidget/Styles.js
================================================
import styled from 'styled-components';

import { color, font } from 'shared/utils/styles';
import { Icon } from 'shared/components';

export const TrackingWidget = styled.div`
  display: flex;
  justify-content: space-between;
  align-items: center;
`;

export const WatchIcon = styled(Icon)`
  color: ${color.textMedium};
`;

export const Right = styled.div`
  width: 90%;
`;

export const BarCont = styled.div`
  height: 5px;
  border-radius: 4px;
  background: ${color.backgroundMedium};
`;

export const Bar = styled.div`
  height: 5px;
  border-radius: 4px;
  background: ${color.primary};
  transition: all 0.1s;
  width: ${props => props.width}%;
`;

export const Values = styled.div`
  display: flex;
  justify-content: space-between;
  padding-top: 3px;
  ${font.size(14.5)};
`;


================================================
FILE: client/src/Project/Board/IssueDetails/EstimateTracking/TrackingWidget/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { isNil } from 'lodash';

import { TrackingWidget, WatchIcon, Right, BarCont, Bar, Values } from './Styles';

const propTypes = {
  issue: PropTypes.object.isRequired,
};

const ProjectBoardIssueDetailsTrackingWidget = ({ issue }) => (
  <TrackingWidget>
    <WatchIcon type="stopwatch" size={26} top={-1} />
    <Right>
      <BarCont>
        <Bar width={calculateTrackingBarWidth(issue)} />
      </BarCont>
      <Values>
        <div>{issue.timeSpent ? `${issue.timeSpent}h logged` : 'No time logged'}</div>
        {renderRemainingOrEstimate(issue)}
      </Values>
    </Right>
  </TrackingWidget>
);

const calculateTrackingBarWidth = ({ timeSpent, timeRemaining, estimate }) => {
  if (!timeSpent) {
    return 0;
  }
  if (isNil(timeRemaining) && isNil(estimate)) {
    return 100;
  }
  if (!isNil(timeRemaining)) {
    return (timeSpent / (timeSpent + timeRemaining)) * 100;
  }
  if (!isNil(estimate)) {
    return Math.min((timeSpent / estimate) * 100, 100);
  }
};

const renderRemainingOrEstimate = ({ timeRemaining, estimate }) => {
  if (isNil(timeRemaining) && isNil(estimate)) {
    return null;
  }
  if (!isNil(timeRemaining)) {
    return <div>{`${timeRemaining}h remaining`}</div>;
  }
  if (!isNil(estimate)) {
    return <div>{`${estimate}h estimated`}</div>;
  }
};

ProjectBoardIssueDetailsTrackingWidget.propTypes = propTypes;

export default ProjectBoardIssueDetailsTrackingWidget;


================================================
FILE: client/src/Project/Board/IssueDetails/EstimateTracking/index.jsx
================================================
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { isNil } from 'lodash';

import { InputDebounced, Modal, Button } from 'shared/components';

import TrackingWidget from './TrackingWidget';
import { SectionTitle } from '../Styles';
import {
  TrackingLink,
  ModalContents,
  ModalTitle,
  Inputs,
  InputCont,
  InputLabel,
  Actions,
} from './Styles';

const propTypes = {
  issue: PropTypes.object.isRequired,
  updateIssue: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsEstimateTracking = ({ issue, updateIssue }) => (
  <Fragment>
    <SectionTitle>Original Estimate (hours)</SectionTitle>
    {renderHourInput('estimate', issue, updateIssue)}

    <SectionTitle>Time Tracking</SectionTitle>
    <Modal
      testid="modal:tracking"
      width={400}
      renderLink={modal => (
        <TrackingLink onClick={modal.open}>
          <TrackingWidget issue={issue} />
        </TrackingLink>
      )}
      renderContent={modal => (
        <ModalContents>
          <ModalTitle>Time tracking</ModalTitle>
          <TrackingWidget issue={issue} />
          <Inputs>
            <InputCont>
              <InputLabel>Time spent (hours)</InputLabel>
              {renderHourInput('timeSpent', issue, updateIssue)}
            </InputCont>
            <InputCont>
              <InputLabel>Time remaining (hours)</InputLabel>
              {renderHourInput('timeRemaining', issue, updateIssue)}
            </InputCont>
          </Inputs>
          <Actions>
            <Button variant="primary" onClick={modal.close}>
              Done
            </Button>
          </Actions>
        </ModalContents>
      )}
    />
  </Fragment>
);

const renderHourInput = (fieldName, issue, updateIssue) => (
  <InputDebounced
    placeholder="Number"
    filter={/^\d{0,6}$/}
    value={isNil(issue[fieldName]) ? '' : issue[fieldName]}
    onChange={stringValue => {
      const value = stringValue.trim() ? Number(stringValue) : null;
      updateIssue({ [fieldName]: value });
    }}
  />
);

ProjectBoardIssueDetailsEstimateTracking.propTypes = propTypes;

export default ProjectBoardIssueDetailsEstimateTracking;


================================================
FILE: client/src/Project/Board/IssueDetails/Loader.jsx
================================================
import React from 'react';
import ContentLoader from 'react-content-loader';

const IssueDetailsLoader = () => (
  <div style={{ padding: 40 }}>
    <ContentLoader
      height={260}
      width={940}
      speed={2}
      primaryColor="#f3f3f3"
      secondaryColor="#ecebeb"
    >
      <rect x="0" y="0" rx="3" ry="3" width="627" height="24" />
      <rect x="0" y="29" rx="3" ry="3" width="506" height="24" />
      <rect x="0" y="77" rx="3" ry="3" width="590" height="16" />
      <rect x="0" y="100" rx="3" ry="3" width="627" height="16" />
      <rect x="0" y="123" rx="3" ry="3" width="480" height="16" />
      <rect x="0" y="187" rx="3" ry="3" width="370" height="16" />
      <circle cx="18" cy="239" r="18" />
      <rect x="46" y="217" rx="3" ry="3" width="548" height="42" />
      <rect x="683" y="3" rx="3" ry="3" width="135" height="14" />
      <rect x="683" y="33" rx="3" ry="3" width="251" height="24" />
      <rect x="683" y="90" rx="3" ry="3" width="135" height="14" />
      <rect x="683" y="120" rx="3" ry="3" width="251" height="24" />
      <rect x="683" y="177" rx="3" ry="3" width="135" height="14" />
      <rect x="683" y="207" rx="3" ry="3" width="251" height="24" />
    </ContentLoader>
  </div>
);

export default IssueDetailsLoader;


================================================
FILE: client/src/Project/Board/IssueDetails/Priority/Styles.js
================================================
import styled, { css } from 'styled-components';

import { color, font } from 'shared/utils/styles';

export const Priority = styled.div`
  display: flex;
  align-items: center;
  ${props =>
    props.isValue &&
    css`
      padding: 3px 4px 3px 0px;
      border-radius: 4px;
      &:hover,
      &:focus {
        background: ${color.backgroundLight};
      }
    `}
`;

export const Label = styled.div`
  padding: 0 3px 0 8px;
  ${font.size(14.5)}
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Priority/index.jsx
================================================
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';

import { IssuePriority, IssuePriorityCopy } from 'shared/constants/issues';
import { Select, IssuePriorityIcon } from 'shared/components';

import { SectionTitle } from '../Styles';
import { Priority, Label } from './Styles';

const propTypes = {
  issue: PropTypes.object.isRequired,
  updateIssue: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => (
  <Fragment>
    <SectionTitle>Priority</SectionTitle>
    <Select
      variant="empty"
      withClearValue={false}
      dropdownWidth={343}
      name="priority"
      value={issue.priority}
      options={Object.values(IssuePriority).map(priority => ({
        value: priority,
        label: IssuePriorityCopy[priority],
      }))}
      onChange={priority => updateIssue({ priority })}
      renderValue={({ value: priority }) => renderPriorityItem(priority, true)}
      renderOption={({ value: priority }) => renderPriorityItem(priority)}
    />
  </Fragment>
);

const renderPriorityItem = (priority, isValue) => (
  <Priority isValue={isValue}>
    <IssuePriorityIcon priority={priority} />
    <Label>{IssuePriorityCopy[priority]}</Label>
  </Priority>
);

ProjectBoardIssueDetailsPriority.propTypes = propTypes;

export default ProjectBoardIssueDetailsPriority;


================================================
FILE: client/src/Project/Board/IssueDetails/Status/Styles.js
================================================
import styled, { css } from 'styled-components';

import { issueStatusColors, issueStatusBackgroundColors, mixin } from 'shared/utils/styles';

export const Status = styled.div`
  text-transform: uppercase;
  transition: all 0.1s;
  ${props => mixin.tag(issueStatusBackgroundColors[props.color], issueStatusColors[props.color])}
  ${props =>
    props.isValue &&
    css`
      padding: 0 12px;
      height: 32px;
      &:hover {
        transform: scale(1.05);
      }
    `}
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Status/index.jsx
================================================
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';

import { IssueStatus, IssueStatusCopy } from 'shared/constants/issues';
import { Select, Icon } from 'shared/components';

import { SectionTitle } from '../Styles';
import { Status } from './Styles';

const propTypes = {
  issue: PropTypes.object.isRequired,
  updateIssue: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (
  <Fragment>
    <SectionTitle>Status</SectionTitle>
    <Select
      variant="empty"
      dropdownWidth={343}
      withClearValue={false}
      name="status"
      value={issue.status}
      options={Object.values(IssueStatus).map(status => ({
        value: status,
        label: IssueStatusCopy[status],
      }))}
      onChange={status => updateIssue({ status })}
      renderValue={({ value: status }) => (
        <Status isValue color={status}>
          <div>{IssueStatusCopy[status]}</div>
          <Icon type="chevron-down" size={18} />
        </Status>
      )}
      renderOption={({ value: status }) => (
        <Status color={status}>{IssueStatusCopy[status]}</Status>
      )}
    />
  </Fragment>
);

ProjectBoardIssueDetailsStatus.propTypes = propTypes;

export default ProjectBoardIssueDetailsStatus;


================================================
FILE: client/src/Project/Board/IssueDetails/Styles.js
================================================
import styled from 'styled-components';

import { color, font } from 'shared/utils/styles';

export const Content = styled.div`
  display: flex;
  padding: 0 30px 60px;
`;

export const Left = styled.div`
  width: 65%;
  padding-right: 50px;
`;

export const Right = styled.div`
  width: 35%;
  padding-top: 5px;
`;

export const TopActions = styled.div`
  display: flex;
  justify-content: space-between;
  padding: 21px 18px 0;
`;

export const TopActionsRight = styled.div`
  display: flex;
  align-items: center;
  & > * {
    margin-left: 4px;
  }
`;

export const SectionTitle = styled.div`
  margin: 24px 0 5px;
  text-transform: uppercase;
  color: ${color.textMedium};
  ${font.size(12.5)}
  ${font.bold}
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Title/Styles.js
================================================
import styled from 'styled-components';

import { color, font } from 'shared/utils/styles';
import { Textarea } from 'shared/components';

export const TitleTextarea = styled(Textarea)`
  margin: 18px 0 0 -8px;
  height: 44px;
  width: 100%;
  textarea {
    padding: 7px 7px 8px;
    line-height: 1.28;
    border: none;
    resize: none;
    background: #fff;
    border: 1px solid transparent;
    box-shadow: 0 0 0 1px transparent;
    transition: background 0.1s;
    ${font.size(24)}
    ${font.medium}
    &:hover:not(:focus) {
      background: ${color.backgroundLight};
    }
  }
`;

export const ErrorText = styled.div`
  padding-top: 4px;
  color: ${color.danger};
  ${font.size(13)}
  ${font.medium}
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Title/index.jsx
================================================
import React, { Fragment, useRef, useState } from 'react';
import PropTypes from 'prop-types';

import { KeyCodes } from 'shared/constants/keyCodes';
import { is, generateErrors } from 'shared/utils/validation';

import { TitleTextarea, ErrorText } from './Styles';

const propTypes = {
  issue: PropTypes.object.isRequired,
  updateIssue: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsTitle = ({ issue, updateIssue }) => {
  const $titleInputRef = useRef();
  const [error, setError] = useState(null);

  const handleTitleChange = () => {
    setError(null);

    const title = $titleInputRef.current.value;
    if (title === issue.title) return;

    const errors = generateErrors({ title }, { title: [is.required(), is.maxLength(200)] });

    if (errors.title) {
      setError(errors.title);
    } else {
      updateIssue({ title });
    }
  };

  return (
    <Fragment>
      <TitleTextarea
        minRows={1}
        placeholder="Short summary"
        defaultValue={issue.title}
        ref={$titleInputRef}
        onBlur={handleTitleChange}
        onKeyDown={event => {
          if (event.keyCode === KeyCodes.ENTER) {
            event.target.blur();
          }
        }}
      />
      {error && <ErrorText>{error}</ErrorText>}
    </Fragment>
  );
};

ProjectBoardIssueDetailsTitle.propTypes = propTypes;

export default ProjectBoardIssueDetailsTitle;


================================================
FILE: client/src/Project/Board/IssueDetails/Type/Styles.js
================================================
import styled from 'styled-components';

import { color, font } from 'shared/utils/styles';
import { Button } from 'shared/components';

export const TypeButton = styled(Button)`
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: ${color.textMedium};
  ${font.size(13)}
`;

export const Type = styled.div`
  display: flex;
  align-items: center;
`;

export const TypeLabel = styled.div`
  padding: 0 5px 0 7px;
  ${font.size(15)}
`;


================================================
FILE: client/src/Project/Board/IssueDetails/Type/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';

import { IssueType, IssueTypeCopy } from 'shared/constants/issues';
import { IssueTypeIcon, Select } from 'shared/components';

import { TypeButton, Type, TypeLabel } from './Styles';

const propTypes = {
  issue: PropTypes.object.isRequired,
  updateIssue: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (
  <Select
    variant="empty"
    dropdownWidth={150}
    withClearValue={false}
    name="type"
    value={issue.type}
    options={Object.values(IssueType).map(type => ({
      value: type,
      label: IssueTypeCopy[type],
    }))}
    onChange={type => updateIssue({ type })}
    renderValue={({ value: type }) => (
      <TypeButton variant="empty" icon={<IssueTypeIcon type={type} />}>
        {`${IssueTypeCopy[type]}-${issue.id}`}
      </TypeButton>
    )}
    renderOption={({ value: type }) => (
      <Type key={type} onClick={() => updateIssue({ type })}>
        <IssueTypeIcon type={type} top={1} />
        <TypeLabel>{IssueTypeCopy[type]}</TypeLabel>
      </Type>
    )}
  />
);

ProjectBoardIssueDetailsType.propTypes = propTypes;

export default ProjectBoardIssueDetailsType;


================================================
FILE: client/src/Project/Board/IssueDetails/index.jsx
================================================
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';

import api from 'shared/utils/api';
import useApi from 'shared/hooks/api';
import { PageError, CopyLinkButton, Button, AboutTooltip } from 'shared/components';

import Loader from './Loader';
import Type from './Type';
import Delete from './Delete';
import Title from './Title';
import Description from './Description';
import Comments from './Comments';
import Status from './Status';
import AssigneesReporter from './AssigneesReporter';
import Priority from './Priority';
import EstimateTracking from './EstimateTracking';
import Dates from './Dates';
import { TopActions, TopActionsRight, Content, Left, Right } from './Styles';

const propTypes = {
  issueId: PropTypes.string.isRequired,
  projectUsers: PropTypes.array.isRequired,
  fetchProject: PropTypes.func.isRequired,
  updateLocalProjectIssues: PropTypes.func.isRequired,
  modalClose: PropTypes.func.isRequired,
};

const ProjectBoardIssueDetails = ({
  issueId,
  projectUsers,
  fetchProject,
  updateLocalProjectIssues,
  modalClose,
}) => {
  const [{ data, error, setLocalData }, fetchIssue] = useApi.get(`/issues/${issueId}`);

  if (!data) return <Loader />;
  if (error) return <PageError />;

  const { issue } = data;

  const updateLocalIssueDetails = fields =>
    setLocalData(currentData => ({ issue: { ...currentData.issue, ...fields } }));

  const updateIssue = updatedFields => {
    api.optimisticUpdate(`/issues/${issueId}`, {
      updatedFields,
      currentFields: issue,
      setLocalData: fields => {
        updateLocalIssueDetails(fields);
        updateLocalProjectIssues(issue.id, fields);
      },
    });
  };

  return (
    <Fragment>
      <TopActions>
        <Type issue={issue} updateIssue={updateIssue} />
        <TopActionsRight>
          <AboutTooltip
            renderLink={linkProps => (
              <Button icon="feedback" variant="empty" {...linkProps}>
                Give feedback
              </Button>
            )}
          />
          <CopyLinkButton variant="empty" />
          <Delete issue={issue} fetchProject={fetchProject} modalClose={modalClose} />
          <Button icon="close" iconSize={24} variant="empty" onClick={modalClose} />
        </TopActionsRight>
      </TopActions>
      <Content>
        <Left>
          <Title issue={issue} updateIssue={updateIssue} />
          <Description issue={issue} updateIssue={updateIssue} />
          <Comments issue={issue} fetchIssue={fetchIssue} />
        </Left>
        <Right>
          <Status issue={issue} updateIssue={updateIssue} />
          <AssigneesReporter issue={issue} updateIssue={updateIssue} projectUsers={projectUsers} />
          <Priority issue={issue} updateIssue={updateIssue} />
          <EstimateTracking issue={issue} updateIssue={updateIssue} />
          <Dates issue={issue} />
        </Right>
      </Content>
    </Fragment>
  );
};

ProjectBoardIssueDetails.propTypes = propTypes;

export default ProjectBoardIssueDetails;


================================================
FILE: client/src/Project/Board/Lists/List/Issue/Styles.js
================================================
import styled, { css } from 'styled-components';
import { Link } from 'react-router-dom';

import { color, font, mixin } from 'shared/utils/styles';
import { Avatar } from 'shared/components';

export const IssueLink = styled(Link)`
  display: block;
  margin-bottom: 5px;
`;

export const Issue = styled.div`
  padding: 10px;
  border-radius: 3px;
  background: #fff;
  box-shadow: 0px 1px 2px 0px rgba(9, 30, 66, 0.25);
  transition: background 0.1s;
  ${mixin.clickable}
  @media (max-width: 1100px) {
    padding: 10px 8px;
  }
  &:hover {
    background: ${color.backgroundLight};
  }
  ${props =>
    props.isBeingDragged &&
    css`
      transform: rotate(3deg);
      box-shadow: 5px 10px 30px 0px rgba(9, 30, 66, 0.15);
    `}
`;

export const Title = styled.p`
  padding-bottom: 11px;
  ${font.size(15)}
  @media (max-width: 1100px) {
    ${font.size(14.5)}
  }
`;

export const Bottom = styled.div`
  display: flex;
  justify-content: space-between;
  align-items: center;
`;

export const Assignees = styled.div`
  display: flex;
  flex-direction: row-reverse;
  margin-left: 2px;
`;

export const AssigneeAvatar = styled(Avatar)`
  margin-left: -2px;
  box-shadow: 0 0 0 2px #fff;
`;


================================================
FILE: client/src/Project/Board/Lists/List/Issue/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { useRouteMatch } from 'react-router-dom';
import { Draggable } from 'react-beautiful-dnd';

import { IssueTypeIcon, IssuePriorityIcon } from 'shared/components';

import { IssueLink, Issue, Title, Bottom, Assignees, AssigneeAvatar } from './Styles';

const propTypes = {
  projectUsers: PropTypes.array.isRequired,
  issue: PropTypes.object.isRequired,
  index: PropTypes.number.isRequired,
};

const ProjectBoardListIssue = ({ projectUsers, issue, index }) => {
  const match = useRouteMatch();

  const assignees = issue.userIds.map(userId => projectUsers.find(user => user.id === userId));

  return (
    <Draggable draggableId={issue.id.toString()} index={index}>
      {(provided, snapshot) => (
        <IssueLink
          to={`${match.url}/issues/${issue.id}`}
          ref={provided.innerRef}
          data-testid="list-issue"
          {...provided.draggableProps}
          {...provided.dragHandleProps}
        >
          <Issue isBeingDragged={snapshot.isDragging && !snapshot.isDropAnimating}>
            <Title>{issue.title}</Title>
            <Bottom>
              <div>
                <IssueTypeIcon type={issue.type} />
                <IssuePriorityIcon priority={issue.priority} top={-1} left={4} />
              </div>
              <Assignees>
                {assignees.map(user => (
                  <AssigneeAvatar
                    key={user.id}
                    size={24}
                    avatarUrl={user.avatarUrl}
                    name={user.name}
                  />
                ))}
              </Assignees>
            </Bottom>
          </Issue>
        </IssueLink>
      )}
    </Draggable>
  );
};

ProjectBoardListIssue.propTypes = propTypes;

export default ProjectBoardListIssue;


================================================
FILE: client/src/Project/Board/Lists/List/Styles.js
================================================
import styled from 'styled-components';

import { color, font, mixin } from 'shared/utils/styles';

export const List = styled.div`
  display: flex;
  flex-direction: column;
  margin: 0 5px;
  min-height: 400px;
  width: 25%;
  border-radius: 3px;
  background: ${color.backgroundLightest};
`;

export const Title = styled.div`
  padding: 13px 10px 17px;
  text-transform: uppercase;
  color: ${color.textMedium};
  ${font.size(12.5)};
  ${mixin.truncateText}
`;

export const IssuesCount = styled.span`
  text-transform: lowercase;
  ${font.size(13)};
`;

export const Issues = styled.div`
  height: 100%;
  padding: 0 5px;
`;


================================================
FILE: client/src/Project/Board/Lists/List/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { Droppable } from 'react-beautiful-dnd';
import { intersection } from 'lodash';

import { IssueStatusCopy } from 'shared/constants/issues';

import Issue from './Issue';
import { List, Title, IssuesCount, Issues } from './Styles';

const propTypes = {
  status: PropTypes.string.isRequired,
  project: PropTypes.object.isRequired,
  filters: PropTypes.object.isRequired,
  currentUserId: PropTypes.number,
};

const defaultProps = {
  currentUserId: null,
};

const ProjectBoardList = ({ status, project, filters, currentUserId }) => {
  const filteredIssues = filterIssues(project.issues, filters, currentUserId);
  const filteredListIssues = getSortedListIssues(filteredIssues, status);
  const allListIssues = getSortedListIssues(project.issues, status);

  return (
    <Droppable key={status} droppableId={status}>
      {provided => (
        <List>
          <Title>
            {`${IssueStatusCopy[status]} `}
            <IssuesCount>{formatIssuesCount(allListIssues, filteredListIssues)}</IssuesCount>
          </Title>
          <Issues
            {...provided.droppableProps}
            ref={provided.innerRef}
            data-testid={`board-list:${status}`}
          >
            {filteredListIssues.map((issue, index) => (
              <Issue key={issue.id} projectUsers={project.users} issue={issue} index={index} />
            ))}
            {provided.placeholder}
          </Issues>
        </List>
      )}
    </Droppable>
  );
};

const filterIssues = (projectIssues, filters, currentUserId) => {
  const { searchTerm, userIds, myOnly, recent } = filters;
  let issues = projectIssues;

  if (searchTerm) {
    issues = issues.filter(issue => issue.title.toLowerCase().includes(searchTerm.toLowerCase()));
  }
  if (userIds.length > 0) {
    issues = issues.filter(issue => intersection(issue.userIds, userIds).length > 0);
  }
  if (myOnly && currentUserId) {
    issues = issues.filter(issue => issue.userIds.includes(currentUserId));
  }
  if (recent) {
    issues = issues.filter(issue => moment(issue.updatedAt).isAfter(moment().subtract(3, 'days')));
  }
  return issues;
};

const getSortedListIssues = (issues, status) =>
  issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition);

const formatIssuesCount = (allListIssues, filteredListIssues) => {
  if (allListIssues.length !== filteredListIssues.length) {
    return `${filteredListIssues.length} of ${allListIssues.length}`;
  }
  return allListIssues.length;
};

ProjectBoardList.propTypes = propTypes;
ProjectBoardList.defaultProps = defaultProps;

export default ProjectBoardList;


================================================
FILE: client/src/Project/Board/Lists/Styles.js
================================================
import styled from 'styled-components';

export const Lists = styled.div`
  display: flex;
  margin: 26px -5px 0;
`;


================================================
FILE: client/src/Project/Board/Lists/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { DragDropContext } from 'react-beautiful-dnd';

import useCurrentUser from 'shared/hooks/currentUser';
import api from 'shared/utils/api';
import { moveItemWithinArray, insertItemIntoArray } from 'shared/utils/javascript';
import { IssueStatus } from 'shared/constants/issues';

import List from './List';
import { Lists } from './Styles';

const propTypes = {
  project: PropTypes.object.isRequired,
  filters: PropTypes.object.isRequired,
  updateLocalProjectIssues: PropTypes.func.isRequired,
};

const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => {
  const { currentUserId } = useCurrentUser();

  const handleIssueDrop = ({ draggableId, destination, source }) => {
    if (!isPositionChanged(source, destination)) return;

    const issueId = Number(draggableId);

    api.optimisticUpdate(`/issues/${issueId}`, {
      updatedFields: {
        status: destination.droppableId,
        listPosition: calculateIssueListPosition(project.issues, destination, source, issueId),
      },
      currentFields: project.issues.find(({ id }) => id === issueId),
      setLocalData: fields => updateLocalProjectIssues(issueId, fields),
    });
  };

  return (
    <DragDropContext onDragEnd={handleIssueDrop}>
      <Lists>
        {Object.values(IssueStatus).map(status => (
          <List
            key={status}
            status={status}
            project={project}
            filters={filters}
            currentUserId={currentUserId}
          />
        ))}
      </Lists>
    </DragDropContext>
  );
};

const isPositionChanged = (destination, source) => {
  if (!destination) return false;
  const isSameList = destination.droppableId === source.droppableId;
  const isSamePosition = destination.index === source.index;
  return !isSameList || !isSamePosition;
};

const calculateIssueListPosition = (...args) => {
  const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(...args);
  let position;

  if (!prevIssue && !nextIssue) {
    position = 1;
  } else if (!prevIssue) {
    position = nextIssue.listPosition - 1;
  } else if (!nextIssue) {
    position = prevIssue.listPosition + 1;
  } else {
    position = prevIssue.listPosition + (nextIssue.listPosition - prevIssue.listPosition) / 2;
  }
  return position;
};

const getAfterDropPrevNextIssue = (allIssues, destination, source, droppedIssueId) => {
  const beforeDropDestinationIssues = getSortedListIssues(allIssues, destination.droppableId);
  const droppedIssue = allIssues.find(issue => issue.id === droppedIssueId);
  const isSameList = destination.droppableId === source.droppableId;

  const afterDropDestinationIssues = isSameList
    ? moveItemWithinArray(beforeDropDestinationIssues, droppedIssue, destination.index)
    : insertItemIntoArray(beforeDropDestinationIssues, droppedIssue, destination.index);

  return {
    prevIssue: afterDropDestinationIssues[destination.index - 1],
    nextIssue: afterDropDestinationIssues[destination.index + 1],
  };
};

const getSortedListIssues = (issues, status) =>
  issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition);

ProjectBoardLists.propTypes = propTypes;

export default ProjectBoardLists;


================================================
FILE: client/src/Project/Board/index.jsx
================================================
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Route, useRouteMatch, useHistory } from 'react-router-dom';

import useMergeState from 'shared/hooks/mergeState';
import { Breadcrumbs, Modal } from 'shared/components';

import Header from './Header';
import Filters from './Filters';
import Lists from './Lists';
import IssueDetails from './IssueDetails';

const propTypes = {
  project: PropTypes.object.isRequired,
  fetchProject: PropTypes.func.isRequired,
  updateLocalProjectIssues: PropTypes.func.isRequired,
};

const defaultFilters = {
  searchTerm: '',
  userIds: [],
  myOnly: false,
  recent: false,
};

const ProjectBoard = ({ project, fetchProject, updateLocalProjectIssues }) => {
  const match = useRouteMatch();
  const history = useHistory();

  const [filters, mergeFilters] = useMergeState(defaultFilters);

  return (
    <Fragment>
      <Breadcrumbs items={['Projects', project.name, 'Kanban Board']} />
      <Header />
      <Filters
        projectUsers={project.users}
        defaultFilters={defaultFilters}
        filters={filters}
        mergeFilters={mergeFilters}
      />
      <Lists
        project={project}
        filters={filters}
        updateLocalProjectIssues={updateLocalProjectIssues}
      />
      <Route
        path={`${match.path}/issues/:issueId`}
        render={routeProps => (
          <Modal
            isOpen
            testid="modal:issue-details"
            width={1040}
            withCloseIcon={false}
            onClose={() => history.push(match.url)}
            renderContent={modal => (
              <IssueDetails
                issueId={routeProps.match.params.issueId}
                projectUsers={project.users}
                fetchProject={fetchProject}
                updateLocalProjectIssues={updateLocalProjectIssues}
                modalClose={modal.close}
              />
            )}
          />
        )}
      />
    </Fragment>
  );
};

ProjectBoard.propTypes = propTypes;

export default ProjectBoard;


================================================
FILE: client/src/Project/IssueCreate/Styles.js
================================================
import styled from 'styled-components';

import { color, font } from 'shared/utils/styles';
import { Button, Form } from 'shared/components';

export const FormElement = styled(Form.Element)`
  padding: 25px 40px 35px;
`;

export const FormHeading = styled.div`
  padding-bottom: 15px;
  ${font.size(21)}
`;

export const SelectItem = styled.div`
  display: flex;
  align-items: center;
  margin-right: 15px;
  ${props => props.withBottomMargin && `margin-bottom: 5px;`}
`;

export const SelectItemLabel = styled.div`
  padding: 0 3px 0 6px;
`;

export const Divider = styled.div`
  margin-top: 22px;
  border-top: 1px solid ${color.borderLightest};
`;

export const Actions = styled.div`
  display: flex;
  justify-content: flex-end;
  padding-top: 30px;
`;

export const ActionButton = styled(Button)`
  margin-left: 10px;
`;


================================================
FILE: client/src/Project/IssueCreate/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';

import {
  IssueType,
  IssueStatus,
  IssuePriority,
  IssueTypeCopy,
  IssuePriorityCopy,
} from 'shared/constants/issues';
import toast from 'shared/utils/toast';
import useApi from 'shared/hooks/api';
import useCurrentUser from 'shared/hooks/currentUser';
import { Form, IssueTypeIcon, Icon, Avatar, IssuePriorityIcon } from 'shared/components';

import {
  FormHeading,
  FormElement,
  SelectItem,
  SelectItemLabel,
  Divider,
  Actions,
  ActionButton,
} from './Styles';

const propTypes = {
  project: PropTypes.object.isRequired,
  fetchProject: PropTypes.func.isRequired,
  onCreate: PropTypes.func.isRequired,
  modalClose: PropTypes.func.isRequired,
};

const ProjectIssueCreate = ({ project, fetchProject, onCreate, modalClose }) => {
  const [{ isCreating }, createIssue] = useApi.post('/issues');

  const { currentUserId } = useCurrentUser();

  return (
    <Form
      enableReinitialize
      initialValues={{
        type: IssueType.TASK,
        title: '',
        description: '',
        reporterId: currentUserId,
        userIds: [],
        priority: IssuePriority.MEDIUM,
      }}
      validations={{
        type: Form.is.required(),
        title: [Form.is.required(), Form.is.maxLength(200)],
        reporterId: Form.is.required(),
        priority: Form.is.required(),
      }}
      onSubmit={async (values, form) => {
        try {
          await createIssue({
            ...values,
            status: IssueStatus.BACKLOG,
            projectId: project.id,
            users: values.userIds.map(id => ({ id })),
          });
          await fetchProject();
          toast.success('Issue has been successfully created.');
          onCreate();
        } catch (error) {
          Form.handleAPIError(error, form);
        }
      }}
    >
      <FormElement>
        <FormHeading>Create issue</FormHeading>
        <Form.Field.Select
          name="type"
          label="Issue Type"
          tip="Start typing to get a list of possible matches."
          options={typeOptions}
          renderOption={renderType}
          renderValue={renderType}
        />
        <Divider />
        <Form.Field.Input
          name="title"
          label="Short Summary"
          tip="Concisely summarize the issue in one or two sentences."
        />
        <Form.Field.TextEditor
          name="description"
          label="Description"
          tip="Describe the issue in as much detail as you'd like."
        />
        <Form.Field.Select
          name="reporterId"
          label="Reporter"
          options={userOptions(project)}
          renderOption={renderUser(project)}
          renderValue={renderUser(project)}
        />
        <Form.Field.Select
          isMulti
          name="userIds"
          label="Assignees"
          tio="People who are responsible for dealing with this issue."
          options={userOptions(project)}
          renderOption={renderUser(project)}
          renderValue={renderUser(project)}
        />
        <Form.Field.Select
          name="priority"
          label="Priority"
          tip="Priority in relation to other issues."
          options={priorityOptions}
          renderOption={renderPriority}
          renderValue={renderPriority}
        />
        <Actions>
          <ActionButton type="submit" variant="primary" isWorking={isCreating}>
            Create Issue
          </ActionButton>
          <ActionButton type="button" variant="empty" onClick={modalClose}>
            Cancel
          </ActionButton>
        </Actions>
      </FormElement>
    </Form>
  );
};

const typeOptions = Object.values(IssueType).map(type => ({
  value: type,
  label: IssueTypeCopy[type],
}));

const priorityOptions = Object.values(IssuePriority).map(priority => ({
  value: priority,
  label: IssuePriorityCopy[priority],
}));

const userOptions = project => project.users.map(user => ({ value: user.id, label: user.name }));

const renderType = ({ value: type }) => (
  <SelectItem>
    <IssueTypeIcon type={type} top={1} />
    <SelectItemLabel>{IssueTypeCopy[type]}</SelectItemLabel>
  </SelectItem>
);

const renderPriority = ({ value: priority }) => (
  <SelectItem>
    <IssuePriorityIcon priority={priority} top={1} />
    <SelectItemLabel>{IssuePriorityCopy[priority]}</SelectItemLabel>
  </SelectItem>
);

const renderUser = project => ({ value: userId, removeOptionValue }) => {
  const user = project.users.find(({ id }) => id === userId);

  return (
    <SelectItem
      key={user.id}
      withBottomMargin={!!removeOptionValue}
      onClick={() => removeOptionValue && removeOptionValue()}
    >
      <Avatar size={20} avatarUrl={user.avatarUrl} name={user.name} />
      <SelectItemLabel>{user.name}</SelectItemLabel>
      {removeOptionValue && <Icon type="close" top={2} />}
    </SelectItem>
  );
};

ProjectIssueCreate.propTypes = propTypes;

export default ProjectIssueCreate;


================================================
FILE: client/src/Project/IssueSearch/NoResultsSvg.jsx
================================================
import React from 'react';

const NoResults = () => (
  <svg
    width="160px"
    height="146px"
    viewBox="0 0 160 146"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
  >
    <defs>
      <linearGradient
        x1="14.2197515%"
        y1="85.3653884%"
        x2="85.2573455%"
        y2="14.6507444%"
        id="linearGradient-1"
      >
        <stop stopColor="#C1C7D0" offset="56%" />
        <stop stopColor="#E9EBEF" stopOpacity="0.5" offset="97%" />
      </linearGradient>
    </defs>
    <g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
      <g transform="translate(-300.000000, -352.000000)" fillRule="nonzero">
        <g id="No-Results" transform="translate(300.000000, 352.000000)">
          <g id="Group" opacity="0.3" transform="translate(8.421053, 5.350785)" fill="#B3BAC5">
            <path
              d="M72.9416268,140.511623 C72.2271132,140.511623 71.5151515,140.526911 70.8057416,140.557487 C69.8121576,140.578595 68.9895612,139.79146 68.9684211,138.799372 C68.947281,137.807283 69.7356026,136.985925 70.7291866,136.964817 L70.7827751,136.964817 C75.3473393,136.941769 79.8977162,136.455146 84.3636364,135.512461 C85.3360803,135.301378 86.2957762,135.917397 86.507177,136.888377 C86.7185779,137.859357 86.1016306,138.817608 85.1291866,139.028691 C81.1172563,139.873967 77.0394498,140.370138 72.9416268,140.511623 Z M61.0679426,139.884817 C60.9635073,139.893719 60.8585022,139.893719 60.754067,139.884817 C55.9858358,139.22527 51.2987018,138.075102 46.7674641,136.45267 C46.161651,136.235561 45.7178575,135.712233 45.6032563,135.079819 C45.4886551,134.447405 45.7206569,133.801983 46.2118688,133.386678 C46.7030807,132.971372 47.3788759,132.849278 47.984689,133.066387 C52.2799672,134.607845 56.7233769,135.701656 61.2440191,136.330366 C62.1763788,136.450209 62.8591494,137.266592 62.8110048,138.203999 C62.7628602,139.141405 62.0000335,139.883767 61.0602871,139.907749 L61.0679426,139.884817 Z M94.2239234,136.315079 C93.3343753,136.355899 92.5487632,135.740786 92.3763355,134.868467 C92.2039078,133.996149 92.6965525,133.129128 93.5349282,132.829424 C97.81401,131.249665 101.918631,129.233743 105.783732,126.813613 C106.330742,126.472246 107.018591,126.448198 107.588173,126.750525 C108.157755,127.052853 108.522538,127.635627 108.545111,128.279321 C108.567683,128.923016 108.244617,129.529838 107.697608,129.871204 C103.622159,132.42303 99.294394,134.549075 94.7827751,136.215707 C94.6041413,136.285729 94.4155129,136.32701 94.2239234,136.33801 L94.2239234,136.315079 Z M38.461244,132.577173 C38.1472102,132.588681 37.8356695,132.517504 37.5578947,132.370785 C33.3113613,130.114297 29.3071112,127.430032 25.6076555,124.359895 C24.8423844,123.726647 24.7361323,122.593857 25.3703349,121.829738 C26.0045375,121.065619 27.1390351,120.959527 27.9043062,121.592775 C31.4168802,124.502776 35.218505,127.046185 39.2497608,129.183246 C39.9718547,129.560888 40.3494187,130.37612 40.1699289,131.170063 C39.9904392,131.964006 39.2987541,132.538235 38.4842105,132.569529 L38.461244,132.577173 Z M19.7435407,118.015393 C19.2124054,118.035471 18.6995628,117.820009 18.3425837,117.426806 C15.118302,113.86169 12.2683329,109.976096 9.83732057,105.83089 C9.51184991,105.276511 9.50730611,104.590869 9.82540078,104.032238 C10.1434954,103.473607 10.7359024,103.126856 11.3794678,103.122604 C12.0230331,103.118352 12.6199839,103.457244 12.9454545,104.011623 C15.2519982,107.943588 17.9557341,111.629375 21.0143541,115.011309 C21.4862704,115.52932 21.6136905,116.27394 21.3408034,116.919019 C21.0679163,117.564099 20.444471,117.992036 19.7435407,118.015393 Z M129.913876,106.136649 C129.260766,106.159641 128.646534,105.827186 128.309369,105.268207 C127.972203,104.709227 127.965136,104.011644 128.290909,103.445969 C130.558266,99.48592 132.410491,95.3029125 133.818182,90.9633508 C134.124713,90.0177005 135.140962,89.4992181 136.088038,89.805288 C137.035114,90.1113578 137.554378,91.1260775 137.247847,92.0717277 C135.760681,96.6402283 133.805853,101.043498 131.414354,105.211728 C131.110187,105.76043 130.541443,106.111014 129.913876,106.136649 Z M7.11961722,97.9423037 C6.35266343,97.9813795 5.64542724,97.5300437 5.35885167,96.8186387 C3.56004271,92.3658752 2.22602943,87.7397184 1.37799043,83.0136126 C1.26311841,82.3814021 1.49476101,81.7360243 1.98566071,81.3205867 C2.4765604,80.9051492 3.15213785,80.7827667 3.75790954,80.9995396 C4.36368124,81.2163125 4.80761606,81.7393079 4.92248804,82.3715183 C5.7438504,86.856095 7.02656307,91.2440537 8.75023923,95.4656545 C8.97381959,96.0103657 8.91591431,96.6298937 8.59524939,97.1238891 C8.27458447,97.6178846 7.73185854,97.9236499 7.14258373,97.9423037 L7.11961722,97.9423037 Z M137.944498,83.8162304 C137.402624,83.835711 136.880854,83.6101211 136.524322,83.2022103 C136.167791,82.7942995 136.014532,82.2475837 136.107177,81.7141361 C136.878794,77.2219928 137.189175,72.6629374 137.033493,68.1078534 L137.033493,67.9549738 C136.997555,66.9628854 137.77388,66.129549 138.767464,66.0936649 C139.761048,66.0577809 140.595641,66.8329377 140.631579,67.8250262 L140.631579,67.9779058 C140.802125,72.7780009 140.481484,77.5829418 139.674641,82.3180105 C139.528583,83.1637528 138.803773,83.7882002 137.944498,83.8085864 L137.944498,83.8162304 Z M2.15885167,74.7122513 C1.68586719,74.737153 1.22264651,74.5715641 0.872962602,74.2525806 C0.52327869,73.9335972 0.316361126,73.4878826 0.298564593,73.015288 L0.298564593,72.9388482 C0.128222327,68.164727 0.443715369,63.3856712 1.24019139,58.6751832 C1.40719807,57.6957597 2.33776029,57.036962 3.31866029,57.2037173 C4.29956028,57.3704726 4.95935118,58.2996341 4.7923445,59.2790576 C4.03647908,63.7471754 3.7363791,68.2803157 3.89665072,72.8089005 C3.93996712,73.8125004 3.16367498,74.6627461 2.15885167,74.7122513 Z M137.799043,59.9517277 C136.90711,59.9811538 136.128388,59.353258 135.969378,58.4764398 C135.14793,53.9943624 133.865212,49.6089472 132.141627,45.3899476 C131.765333,44.4632948 132.21262,43.4075072 133.14067,43.0317801 C134.06872,42.656053 135.126099,43.1026665 135.502392,44.0293194 C137.314223,48.4806659 138.661072,53.1068537 139.521531,57.8343455 C139.608602,58.303758 139.504956,58.7884702 139.23348,59.1814453 C138.962005,59.5744203 138.545032,59.8433313 138.074641,59.9287958 L137.799043,59.9517277 Z M5.41244019,51.2146597 C4.8390386,51.22975 4.29280011,50.9709175 3.94190122,50.517852 C3.59100233,50.0647864 3.47750899,49.4718015 3.63636364,48.921466 C5.11835359,44.3510171 7.06804294,39.9452041 9.45454545,35.773822 C9.77317849,35.2153466 10.3659998,34.869027 11.0096992,34.8653183 C11.6533987,34.8616095 12.2501831,35.2010752 12.5752495,35.7558418 C12.9003159,36.3106085 12.904279,36.9963938 12.5856459,37.5548691 C10.3214091,41.5127965 8.47176357,45.6932096 7.06602871,50.0298429 C6.81304549,50.7280643 6.15590233,51.1989156 5.41244019,51.2146597 Z M129.546411,37.7306806 C128.885706,37.7539169 128.265468,37.4134462 127.9311,36.8439791 C125.619468,32.9138609 122.910582,29.2306094 119.84689,25.8519372 C119.33883,25.3947473 119.134344,24.6894473 119.319229,24.0319584 C119.504115,23.3744694 120.046389,22.8785256 120.71853,22.7522091 C121.390672,22.6258926 122.076413,22.8910539 122.488038,23.4364398 C125.7168,26.9960227 128.571892,30.876485 131.008612,35.0170681 C131.329911,35.5624175 131.340181,36.2363285 131.035648,36.7911862 C130.731115,37.3460439 130.156694,37.7000137 129.523445,37.7230366 L129.546411,37.7306806 Z M16.5435407,30.300733 C15.842639,30.3281799 15.1896729,29.9463624 14.8707293,29.3225678 C14.5517856,28.6987733 14.6250307,27.9467639 15.0583732,27.3960209 C18.0142096,23.6097744 21.348824,20.1341894 25.0105263,17.0231414 C25.4873084,16.5240558 26.208179,16.3416218 26.8654815,16.5536994 C27.522784,16.7657771 28.0004307,17.3349108 28.0945693,18.0182031 C28.1887079,18.7014954 27.8827124,19.3782685 27.307177,19.7596859 C23.8448469,22.7145731 20.6924269,26.0133568 17.8985646,29.6051309 C17.5723727,30.0259545 17.0761061,30.2807131 16.5435407,30.300733 Z M114.158852,19.6679581 C113.718544,19.6835688 113.287917,19.536614 112.949282,19.2551832 C109.433973,16.3451756 105.62981,13.8017853 101.596172,11.664712 C100.716745,11.2003302 100.380851,10.1120305 100.845933,9.23392674 C101.311015,8.35582293 102.400955,8.02043497 103.280383,8.48481675 C107.528709,10.7363471 111.535486,13.4154639 115.238278,16.4804188 C115.814731,16.9562193 116.03624,17.7385032 115.794632,18.4452463 C115.553024,19.1519895 114.8987,19.6357612 114.151196,19.6603141 L114.158852,19.6679581 Z M34.1588517,14.3936126 C33.3439602,14.4258291 32.6092952,13.9066881 32.3687024,13.1286278 C32.1281097,12.3505676 32.4417781,11.5082347 33.1330144,11.0761257 C37.2076908,8.51877623 41.5355024,6.38758523 46.0478469,4.71633508 C46.9801247,4.37227035 48.0152252,4.84797367 48.3598086,5.77884817 C48.704392,6.70972267 48.2279716,7.74326512 47.2956938,8.08732984 C43.0160796,9.67215682 38.911457,11.6932021 35.04689,14.1184293 C34.780294,14.2866024 34.4739619,14.3815279 34.1588517,14.3936126 Z M93.5196172,7.9191623 C93.2928357,7.92574274 93.066854,7.8894326 92.8535885,7.8121466 C88.5640172,6.28621494 84.1285164,5.20526416 79.6172249,4.58638743 C78.9799588,4.49899766 78.4377928,4.07878051 78.1949556,3.48402772 C77.9521184,2.88927492 78.0455028,2.21034359 78.4399317,1.70298058 C78.8343606,1.19561757 79.4699109,0.936903397 80.107177,1.02429319 C84.8744687,1.67699297 89.5615235,2.81945593 94.0937799,4.43350785 C94.9158919,4.71763866 95.4166006,5.54844339 95.2830439,6.40680323 C95.1494872,7.26516307 94.4199348,7.90512085 93.5502392,7.92680628 L93.5196172,7.9191623 Z M56.1607656,5.47308901 C55.2240403,5.51196427 54.4144474,4.82603155 54.3003777,3.89686354 C54.186308,2.96769553 54.8060066,2.10682306 55.7244019,1.91863874 C60.4299958,0.928891774 65.2237475,0.416688898 70.0325359,0.389842932 C71.0261199,0.368734667 71.8487164,1.15586965 71.8698565,2.14795812 C71.8909965,3.14004658 71.102675,3.96140482 70.1090909,3.98251309 L70.0555024,3.98251309 C65.4903705,4.0097933 60.9399074,4.50154855 56.4746411,5.45015707 C56.3708786,5.46686412 56.2658585,5.47453695 56.1607656,5.47308901 Z"
              id="Shape"
            />
          </g>
          <g id="Group" opacity="0.3" transform="translate(143.157895, 9.937173)" fill="#C1C7D0">
            <path
              d="M3.67464115,16.9619895 C3.4821549,16.9135821 3.35479614,16.7309832 3.37607656,16.5339267 C3.37607656,16.416719 3.39138756,16.3020593 3.42200957,16.1899476 C3.45863702,15.9532422 3.50718339,15.7185294 3.56746411,15.4867016 C3.79659095,14.6127055 4.2264482,13.8040107 4.82296651,13.124712 C5.54164908,12.3515449 6.43366256,11.7595108 7.42583732,11.3971728 L9.32440191,10.6327749 C10.3538957,10.2884796 11.1435596,9.45412946 11.4296651,8.40837696 C11.5279721,8.03200848 11.5590699,7.64128405 11.5215311,7.25413613 C11.4857083,6.86014201 11.3628218,6.47898394 11.1617225,6.13811518 C10.9405401,5.7712585 10.6460674,5.45381082 10.2966507,5.20554974 C9.87838479,4.91141537 9.40827303,4.69875715 8.91100478,4.57874346 C8.39121626,4.42619824 7.84200245,4.40258349 7.31100478,4.50994764 C6.85534038,4.60935206 6.42524698,4.80195209 6.04784689,5.07560209 C5.67500081,5.35721252 5.36260232,5.71073122 5.1291866,6.11518325 C4.88093408,6.52451937 4.69273575,6.96727812 4.57033493,7.42994764 C4.53460925,7.57263525 4.50398724,7.70513089 4.4784689,7.82743455 C4.44440318,7.9938222 4.34257429,8.13857109 4.19737224,8.22701101 C4.05217019,8.31545094 3.87667408,8.33961567 3.71291866,8.29371728 L0.765550239,7.37643979 C0.486479438,7.29689199 0.303343066,7.03062114 0.329186603,6.74198953 C0.329186603,6.65026178 0.341945774,6.55853403 0.367464115,6.46680628 C0.413397129,6.20691099 0.466985646,5.95211169 0.528229665,5.70240838 C0.761538461,4.81855845 1.15884083,3.98625719 1.69952153,3.2486911 C2.2461278,2.49616236 2.94187651,1.86390599 3.74354067,1.39120419 C4.59527983,0.895024603 5.53729439,0.572831293 6.51483254,0.443350785 C7.65634614,0.300278092 8.81503633,0.388673973 9.9215311,0.703246073 C11.0732655,0.982980352 12.1633298,1.4727709 13.1368421,2.14795812 C13.9461813,2.71460259 14.6420701,3.4275928 15.1885167,4.25005236 C15.6781959,4.99991063 15.997025,5.84797194 16.122488,6.73434555 C16.2459207,7.59159451 16.1964667,8.46483262 15.9770335,9.30272251 C15.7003047,10.4988535 15.0835814,11.5897662 14.2009569,12.4443979 C13.3220893,13.2446358 12.2947848,13.8652844 11.1770335,14.2713089 L9.57703349,14.8751832 C8.93416496,15.0764351 8.35410174,15.4397453 7.89282297,15.9300524 C7.69795446,16.1461264 7.52606151,16.3818032 7.37990431,16.6332984 C7.22708163,16.8923557 7.10137893,17.1664362 7.00478469,17.4512042 C6.93573802,17.6490764 6.72471173,17.7594476 6.52248804,17.7034555 L3.67464115,16.9619895 Z M1.33205742,21.4490052 C1.50522429,20.7498658 1.9556721,20.1510925 2.57990431,19.7902618 C3.19276119,19.4104991 3.93714845,19.3051106 4.63157895,19.4997906 C6.08589311,19.8847884 6.95504957,21.3710638 6.57607656,22.8249215 C6.40284896,23.5226722 5.94468797,24.1159986 5.31291866,24.460733 C4.69126881,24.8252266 3.9479522,24.9217951 3.25358852,24.7282723 C2.55992232,24.5505947 1.96635409,24.1030508 1.60533978,23.485511 C1.24432547,22.8679712 1.14591088,22.1318309 1.33205742,21.4413613 L1.33205742,21.4490052 Z"
              id="Shape"
            />
          </g>
          <g id="Group" opacity="0.3" transform="translate(11.483254, 0.000000)" fill="#C1C7D0">
            <path
              d="M3.45263158,17.0613613 C3.26593813,16.9999045 3.15212274,16.81158 3.184689,16.6180105 C3.184689,16.5008028 3.2076555,16.3861431 3.25358852,16.2740314 C3.30835701,16.0415287 3.37478841,15.81192 3.45263158,15.5860733 C3.7384919,14.7215446 4.2235907,13.936082 4.86889952,13.2928796 C5.64134788,12.5763691 6.5726126,12.0525027 7.58660287,11.7640838 L9.53110048,11.1219895 C10.5836438,10.845994 11.4283045,10.0628468 11.7818182,9.03518325 C11.905053,8.66584805 11.962067,8.27770094 11.9502392,7.88858639 C11.9414039,7.49268261 11.8446589,7.10367421 11.6669856,6.74963351 C11.4809249,6.37281104 11.2205318,6.03740952 10.9014354,5.76356021 C10.5041083,5.44203303 10.0495852,5.19825673 9.56172249,5.04502618 C9.05317702,4.85791539 8.50657949,4.79756529 7.96937799,4.86921466 C7.49095048,4.93175557 7.03150705,5.09596788 6.62200957,5.35078534 C6.23189645,5.60299356 5.89614613,5.93044209 5.63444976,6.3139267 C5.36007328,6.70655383 5.14351426,7.13644573 4.99138756,7.5904712 C4.94545455,7.72806283 4.90717703,7.85801047 4.87655502,7.98031414 C4.83556205,8.15257986 4.72180274,8.29864795 4.56469072,8.38075103 C4.4075787,8.46285411 4.22253928,8.47293127 4.05741627,8.40837696 L1.20191388,7.30764398 C0.928565345,7.20852926 0.764568254,6.92919062 0.811483254,6.6426178 C0.811483254,6.55089005 0.831897927,6.4591623 0.872727273,6.36743455 C0.933971292,6.11263525 1.00542265,5.85783595 1.08708134,5.60303665 C1.38113007,4.7363737 1.83557828,3.9325629 2.42679426,3.23340314 C3.02456414,2.52318903 3.76194343,1.94301918 4.59330144,1.52879581 C5.47496364,1.08978267 6.43516038,0.829958436 7.41818182,0.764397906 C8.56502592,0.699399832 9.71323,0.865781802 10.7942584,1.25361257 C11.9264287,1.60990465 12.9828054,2.17228468 13.9100478,2.91235602 C14.6801095,3.53334768 15.3266937,4.29304259 15.8162679,5.15204188 C16.2541878,5.93192239 16.5155082,6.79820023 16.5818182,7.68984293 C16.6461737,8.5537654 16.5368532,9.42181532 16.2602871,10.2429319 C15.9011343,11.4172901 15.2099672,12.4630977 14.2698565,13.2546597 C13.3395541,13.9941991 12.2732479,14.5447275 11.1311005,14.8751832 L9.49282297,15.3643979 C8.83717925,15.5219973 8.23334866,15.8458373 7.73971292,16.3046073 C7.53094535,16.5064335 7.34366329,16.7292967 7.18086124,16.9696335 C7.01273243,17.2094863 6.86911145,17.4655663 6.75215311,17.7340314 C6.6708489,17.9274513 6.45301088,18.0244943 6.25454545,17.9557068 L3.45263158,17.0613613 Z M0.811483254,21.4031414 C1.0301283,20.7162284 1.51932402,20.147739 2.16650718,19.8284817 C2.80354241,19.490343 3.55351387,19.4348732 4.23349282,19.6756021 C5.65767373,20.1579331 6.42399623,21.6985189 5.94832536,23.1230366 C5.72407846,23.8005858 5.22763256,24.354439 4.57799043,24.6518325 C3.93283017,24.9747315 3.18386522,25.0216438 2.50334928,24.7817801 C1.10346144,24.313332 0.346873956,22.8024348 0.811483254,21.4031414 Z"
              id="Shape"
            />
          </g>
          <g id="Group" opacity="0.3" transform="translate(0.000000, 120.774869)" fill="#C1C7D0">
            <path
              d="M10.2507177,17.871623 C10.066335,17.9425541 9.85775094,17.867707 9.76076555,17.6958115 L9.6,17.3824084 C9.49462399,17.1681521 9.40008714,16.9487485 9.31674641,16.7250262 C9.00232179,15.8783684 8.88961829,14.9702464 8.98755981,14.0725654 C9.13139661,13.0252725 9.52235365,12.0270818 10.1282297,11.1602094 L11.2382775,9.44031414 C11.8906538,8.55973781 12.0547098,7.41020078 11.6746411,6.38272251 C11.5388014,6.0168501 11.3392378,5.67784396 11.0851675,5.38136126 C10.8303228,5.07835152 10.511338,4.83555702 10.1511962,4.6704712 C9.75812295,4.49648378 9.33330672,4.40540287 8.90334928,4.40293194 C8.37949854,4.39781711 7.85959596,4.49385431 7.3722488,4.68575916 C6.85670555,4.85887004 6.39147548,5.15543425 6.01722488,5.5495288 C5.71514577,5.90717224 5.49093243,6.32368543 5.35885167,6.77256545 C5.21204305,7.2129125 5.15478303,7.67809961 5.19043062,8.1408377 C5.22239525,8.61898427 5.32294243,9.09007244 5.48899522,9.53968586 C5.5400319,9.67727749 5.59106858,9.80212914 5.64210526,9.91424084 C5.7138017,10.0725062 5.71470723,10.2537487 5.64459577,10.4127205 C5.57448431,10.5716923 5.43997232,10.6933912 5.27464115,10.7474346 L2.36555024,11.695288 C2.09062434,11.7877689 1.78865769,11.6722962 1.64593301,11.4201047 L1.53110048,11.1678534 C1.41881978,10.933438 1.31929825,10.6990227 1.23253589,10.4646073 C0.908933764,9.60498005 0.750506251,8.69215148 0.765550239,7.7739267 C0.780423155,6.84513628 0.98627494,5.92930389 1.37033493,5.08324607 C1.77320845,4.19246647 2.34814295,3.38980879 3.06220096,2.72125654 C3.9152958,1.9513887 4.91513427,1.36122803 6.00191388,0.986073298 C7.10451578,0.553622754 8.27760911,0.328168114 9.46220096,0.32104712 C10.4522984,0.31965958 11.4338689,0.503751116 12.3559809,0.863769634 C13.18959,1.20627135 13.9372015,1.72828967 14.5454545,2.39256545 C15.1503053,3.02342252 15.6215753,3.7695601 15.9311005,4.58638743 C16.3926896,5.725056 16.5149772,6.97263503 16.2832536,8.17905759 C16.0247603,9.33940426 15.5410005,10.4379118 14.8593301,11.4124607 L13.8947368,12.8265969 C13.4869168,13.3599654 13.2232936,13.9890762 13.1291866,14.6535079 C13.0994859,14.9406674 13.0994859,15.2301179 13.1291866,15.5172775 C13.1551185,15.8152702 13.2115534,16.1098267 13.2976077,16.3963351 C13.3164748,16.5861901 13.1975855,16.7626515 13.0143541,16.8167539 L10.2507177,17.871623 Z M10.9090909,22.8937173 C10.6475562,22.220209 10.672439,21.4693276 10.9779904,20.814555 C11.2654808,20.1484526 11.8216387,19.6347809 12.5090909,19.4004188 C13.9232856,18.8810016 15.4917113,19.6024857 16.015311,21.0132984 C16.2753556,21.6837696 16.2420338,22.4323799 15.923445,23.0771728 C15.6241489,23.7314594 15.0715577,24.2363193 14.3923445,24.4760209 C13.7225521,24.7272603 12.9799456,24.7005809 12.3299687,24.4019267 C11.6799917,24.1032725 11.1766412,23.5574541 10.9320574,22.8860733 L10.9090909,22.8937173 Z"
              id="Shape"
            />
          </g>
          <path
            d="M115.108134,102.880314 L109.63445,97.5830366 L102.262201,105.227016 L107.735885,110.524293 C109.133421,111.876478 110.118252,113.596675 110.576077,115.485236 C111.033901,117.373796 112.018732,119.093994 113.416268,120.446178 L135.617225,141.925759 C137.233225,143.48915 139.405129,144.34736 141.654958,144.31152 C143.904787,144.27568 146.048167,143.348727 147.613397,141.73466 C149.179144,140.121093 150.038648,137.952458 150.002754,135.706015 C149.96686,133.459573 149.03851,131.319418 147.42201,129.756545 L125.221053,108.276963 C123.821794,106.925049 122.067076,105.997323 120.160766,105.601571 C118.252095,105.191507 116.499663,104.247679 115.108134,102.880314 Z"
            id="Shape"
            fill="#CFD4DB"
          />
          <path
            d="M119.946411,105.486911 C118.127823,105.066663 116.457992,104.16034 115.115789,102.865026 L113.44689,101.252147 C112.079366,99.9372034 110.113151,99.4506129 108.288898,99.975669 C106.464645,100.500725 105.059502,101.957659 104.602773,103.797659 C104.146045,105.637658 104.707117,107.581183 106.074641,108.896126 L107.743541,110.509005 C109.082215,111.805584 110.04277,113.44109 110.522488,115.240628 C114.142246,112.486792 117.320106,109.197722 119.946411,105.486911 Z"
            id="Shape"
            fill="#DFE1E5"
            style={{ mixBlendMode: 'multiply' }}
          />
          <path
            d="M78.8516746,120.010471 C54.5201756,120.184949 34.593323,100.748551 34.1984139,76.4562496 C33.8035048,52.1639488 53.0880882,32.0920254 77.4124402,31.4779058 C89.1754429,31.2567399 100.538247,35.7432734 108.968421,43.9375916 C121.849408,56.3927091 125.949742,75.3681653 119.358022,92.0184082 C112.766302,108.668651 96.7801668,119.71593 78.8516746,120.010471 Z M77.5502392,42.0418848 C61.4460779,42.3028263 47.7691035,53.8861084 44.8835113,69.7079398 C41.9979191,85.5297712 50.7073746,101.183618 65.6855839,107.096322 C80.6637932,113.009026 97.739179,107.533842 106.469237,94.0191357 C115.199296,80.5044292 113.15262,62.7141774 101.580861,51.5280628 C95.1611126,45.2880999 86.5078568,41.8721948 77.5502392,42.0418848 Z"
            id="_Compound_Clipping_Path_"
            fill="url(#linearGradient-1)"
          />
          <path
            d="M91.8047847,65.9063874 L87.7167464,61.9162304 C87.4295606,61.6291576 87.039861,61.467855 86.6334928,61.467855 C86.2271246,61.467855 85.8374251,61.6291576 85.5502392,61.9162304 L78.0861244,69.5602094 L70.430622,62.053822 C70.1434362,61.7667492 69.7537366,61.6054466 69.3473684,61.6054466 C68.9410002,61.6054466 68.5513007,61.7667492 68.2641148,62.053822 L64.2449761,66.1357068 C63.9574706,66.4224604 63.7959247,66.8115733 63.7959247,67.2173298 C63.7959247,67.6230864 63.9574706,68.0121993 64.2449761,68.2989529 L71.9004785,75.7824084 L64.4057416,83.4263874 C64.1182361,83.713141 63.9566903,84.1022539 63.9566903,84.5080105 C63.9566903,84.913767 64.1182361,85.3028799 64.4057416,85.5896335 L68.4937799,89.6027225 C68.7809658,89.8897953 69.1706653,90.0510979 69.5770335,90.0510979 C69.9834017,90.0510979 70.3731012,89.8897953 70.6602871,89.6027225 L78.1550239,81.9587435 L85.8105263,89.442199 C86.0977122,89.7292717 86.4874117,89.8905744 86.8937799,89.8905744 C87.3001481,89.8905744 87.6898476,89.7292717 87.9770335,89.442199 L91.9961722,85.3603141 C92.2836778,85.0735606 92.4452236,84.6844476 92.4452236,84.2786911 C92.4452236,83.8729346 92.2836778,83.4838216 91.9961722,83.1970681 L84.3406699,75.7136126 L91.8354067,68.0696335 C92.1188909,67.7788418 92.2749544,67.3874411 92.2692105,66.9816721 C92.2634666,66.5759031 92.0963866,66.189063 91.8047847,65.9063874 Z"
            id="_Clipping_Path_"
            fill="#C1C7D0"
          />
        </g>
      </g>
    </g>
  </svg>
);

export default NoResults;


================================================
FILE: client/src/Project/IssueSearch/Styles.js
================================================
import styled from 'styled-components';

import { color, font, mixin } from 'shared/utils/styles';
import { InputDebounced, Spinner, Icon } from 'shared/components';

export const IssueSearch = styled.div`
  padding: 25px 35px 60px;
`;

export const SearchInputCont = styled.div`
  position: relative;
  padding-right: 30px;
  margin-bottom: 40px;
`;

export const SearchInputDebounced = styled(InputDebounced)`
  height: 40px;
  input {
    padding: 0 0 0 32px;
    border: none;
    border-bottom: 2px solid ${color.primary};
    background: #fff;
    ${font.size(21)}
    &:focus,
    &:hover {
      box-shadow: none;
      border: none;
      border-bottom: 2px solid ${color.primary};
      background: #fff;
    }
  }
`;

export const SearchIcon = styled(Icon)`
  position: absolute;
  top: 8px;
  left: 0;
  color: ${color.textMedium};
`;

export const SearchSpinner = styled(Spinner)`
  position: absolute;
  top: 5px;
  right: 30px;
`;

export const Issue = styled.div`
  display: flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 4px;
  transition: background 0.1s;
  ${mixin.clickable}
  &:hover {
    background: ${color.backgroundLight};
  }
`;

export const IssueData = styled.div`
  padding-left: 15px;
`;

export const IssueTitle = styled.div`
  color: ${color.textDark};
  ${font.size(15)}
`;

export const IssueTypeId = styled.div`
  text-transform: uppercase;
  color: ${color.textMedium};
  ${font.size(12.5)}
`;

export const SectionTitle = styled.div`
  padding-bottom: 12px;
  text-transform: uppercase;
  color: ${color.textMedium};
  ${font.bold}
  ${font.size(11.5)}
`;

export const NoResults = styled.div`
  padding-top: 50px;
  text-align: center;
`;

export const NoResultsTitle = styled.div`
  padding-top: 30px;
  ${font.medium}
  ${font.size(20)}
`;

export const NoResultsTip = styled.div`
  padding-top: 10px;
  ${font.size(15)}
`;


================================================
FILE: client/src/Project/IssueSearch/index.jsx
================================================
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { get } from 'lodash';

import useApi from 'shared/hooks/api';
import { sortByNewest } from 'shared/utils/javascript';
import { IssueTypeIcon } from 'shared/components';

import NoResultsSVG from './NoResultsSvg';
import {
  IssueSearch,
  SearchInputCont,
  SearchInputDebounced,
  SearchIcon,
  SearchSpinner,
  Issue,
  IssueData,
  IssueTitle,
  IssueTypeId,
  SectionTitle,
  NoResults,
  NoResultsTitle,
  NoResultsTip,
} from './Styles';

const propTypes = {
  project: PropTypes.object.isRequired,
};

const ProjectIssueSearch = ({ project }) => {
  const [isSearchTermEmpty, setIsSearchTermEmpty] = useState(true);

  const [{ data, isLoading }, fetchIssues] = useApi.get('/issues', {}, { lazy: true });

  const matchingIssues = get(data, 'issues', []);

  const recentIssues = sortByNewest(project.issues, 'createdAt').slice(0, 10);

  const handleSearchChange = value => {
    const searchTerm = value.trim();

    setIsSearchTermEmpty(!searchTerm);

    if (searchTerm) {
      fetchIssues({ searchTerm });
    }
  };

  return (
    <IssueSearch>
      <SearchInputCont>
        <SearchInputDebounced
          autoFocus
          placeholder="Search issues by summary, description..."
          onChange={handleSearchChange}
        />
        <SearchIcon type="search" size={22} />
        {isLoading && <SearchSpinner />}
      </SearchInputCont>

      {isSearchTermEmpty && recentIssues.length > 0 && (
        <Fragment>
          <SectionTitle>Recent Issues</SectionTitle>
          {recentIssues.map(renderIssue)}
        </Fragment>
      )}

      {!isSearchTermEmpty && matchingIssues.length > 0 && (
        <Fragment>
          <SectionTitle>Matching Issues</SectionTitle>
          {matchingIssues.map(renderIssue)}
        </Fragment>
      )}

      {!isSearchTermEmpty && !isLoading && matchingIssues.length === 0 && (
        <NoResults>
          <NoResultsSVG />
          <NoResultsTitle>We couldn&apos;t find anything matching your search</NoResultsTitle>
          <NoResultsTip>Try again with a different term.</NoResultsTip>
        </NoResults>
      )}
    </IssueSearch>
  );
};

const renderIssue = issue => (
  <Link key={issue.id} to={`/project/board/issues/${issue.id}`}>
    <Issue>
      <IssueTypeIcon type={issue.type} size={25} />
      <IssueData>
        <IssueTitle>{issue.title}</IssueTitle>
        <IssueTypeId>{`${issue.type}-${issue.id}`}</IssueTypeId>
      </IssueData>
    </Issue>
  </Link>
);

ProjectIssueSearch.propTypes = propTypes;

export default ProjectIssueSearch;


================================================
FILE: client/src/Project/NavbarLeft/Styles.js
================================================
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';

import { font, sizes, color, mixin, zIndexValues } from 'shared/utils/styles';
import { Logo } from 'shared/components';

export const NavLeft = styled.aside`
  z-index: ${zIndexValues.navLeft};
  position: fixed;
  top: 0;
  left: 0;
  overflow-x: hidden;
  height: 100vh;
  width: ${sizes.appNavBarLeftWidth}px;
  background: ${color.backgroundDarkPrimary};
  transition: all 0.1s;
  ${mixin.hardwareAccelerate}
  &:hover {
    width: 200px;
    box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.6);
  }
`;

export const LogoLink = styled(NavLink)`
  display: block;
  position: relative;
  left: 0;
  margin: 20px 0 10px;
  transition: left 0.1s;
`;

export const StyledLogo = styled(Logo)`
  display: inline-block;
  margin-left: 8px;
  padding: 10px;
  ${mixin.clickable}
`;

export const Bottom = styled.div`
  position: absolute;
  bottom: 20px;
  left: 0;
  width: 100%;
`;

export const Item = styled.div`
  position: relative;
  width: 100%;
  height: 42px;
  line-height: 42px;
  padding-left: 64px;
  color: #deebff;
  transition: color 0.1s;
  ${mixin.clickable}
  &:hover {
    background: rgba(255, 255, 255, 0.1);
  }
  i {
    position: absolute;
    left: 18px;
  }
`;

export const ItemText = styled.div`
  position: relative;
  right: 12px;
  visibility: hidden;
  opacity: 0;
  text-transform: uppercase;
  transition: all 0.1s;
  transition-property: right, visibility, opacity;
  ${font.bold}
  ${font.size(12)}
  ${NavLeft}:hover & {
    right: 0;
    visibility: visible;
    opacity: 1;
  }
`;


================================================
FILE: client/src/Project/NavbarLeft/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';

import { Icon, AboutTooltip } from 'shared/components';

import { NavLeft, LogoLink, StyledLogo, Bottom, Item, ItemText } from './Styles';

const propTypes = {
  issueSearchModalOpen: PropTypes.func.isRequired,
  issueCreateModalOpen: PropTypes.func.isRequired,
};

const ProjectNavbarLeft = ({ issueSearchModalOpen, issueCreateModalOpen }) => (
  <NavLeft>
    <LogoLink to="/">
      <StyledLogo color="#fff" />
    </LogoLink>

    <Item onClick={issueSearchModalOpen}>
      <Icon type="search" size={22} top={1} left={3} />
      <ItemText>Search issues</ItemText>
    </Item>

    <Item onClick={issueCreateModalOpen}>
      <Icon type="plus" size={27} />
      <ItemText>Create Issue</ItemText>
    </Item>

    <Bottom>
      <AboutTooltip
        placement="right"
        offset={{ top: -218 }}
        renderLink={linkProps => (
          <Item {...linkProps}>
            <Icon type="help" size={25} />
            <ItemText>About</ItemText>
          </Item>
        )}
      />
    </Bottom>
  </NavLeft>
);

ProjectNavbarLeft.propTypes = propTypes;

export default ProjectNavbarLeft;


================================================
FILE: client/src/Project/ProjectSettings/Styles.js
================================================
import styled from 'styled-components';

import { font } from 'shared/utils/styles';
import { Button, Form } from 'shared/components';

export const FormCont = styled.div`
  display: flex;
  justify-content: center;
`;

export const FormElement = styled(Form.Element)`
  width: 100%;
  max-width: 640px;
`;

export const FormHeading = styled.h1`
  padding: 6px 0 15px;
  ${font.size(24)}
  ${font.medium}
`;

export const ActionButton = styled(Button)`
  margin-top: 30px;
`;


================================================
FILE: client/src/Project/ProjectSettings/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';

import { ProjectCategory, ProjectCategoryCopy } from 'shared/constants/projects';
import toast from 'shared/utils/toast';
import useApi from 'shared/hooks/api';
import { Form, Breadcrumbs } from 'shared/components';

import { FormCont, FormHeading, FormElement, ActionButton } from './Styles';

const propTypes = {
  project: PropTypes.object.isRequired,
  fetchProject: PropTypes.func.isRequired,
};

const ProjectSettings = ({ project, fetchProject }) => {
  const [{ isUpdating }, updateProject] = useApi.put('/project');

  return (
    <Form
      initialValues={Form.initialValues(project, get => ({
        name: get('name'),
        url: get('url'),
        category: get('category'),
        description: get('description'),
      }))}
      validations={{
        name: [Form.is.required(), Form.is.maxLength(100)],
        url: Form.is.url(),
        category: Form.is.required(),
      }}
      onSubmit={async (values, form) => {
        try {
          await updateProject(values);
          await fetchProject();
          toast.success('Changes have been saved successfully.');
        } catch (error) {
          Form.handleAPIError(error, form);
        }
      }}
    >
      <FormCont>
        <FormElement>
          <Breadcrumbs items={['Projects', project.name, 'Project Details']} />
          <FormHeading>Project Details</FormHeading>

          <Form.Field.Input name="name" label="Name" />
          <Form.Field.Input name="url" label="URL" />
          <Form.Field.TextEditor
            name="description"
            label="Description"
            tip="Describe the project in as much detail as you'd like."
          />
          <Form.Field.Select name="category" label="Project Category" options={categoryOptions} />

          <ActionButton type="submit" variant="primary" isWorking={isUpdating}>
            Save changes
          </ActionButton>
        </FormElement>
      </FormCont>
    </Form>
  );
};

const categoryOptions = Object.values(ProjectCategory).map(category => ({
  value: category,
  label: ProjectCategoryCopy[category],
}));

ProjectSettings.propTypes = propTypes;

export default ProjectSettings;


================================================
FILE: client/src/Project/Sidebar/Styles.js
================================================
import styled from 'styled-components';

import { color, sizes, font, mixin, zIndexValues } from 'shared/utils/styles';

export const Sidebar = styled.div`
  position: fixed;
  z-index: ${zIndexValues.navLeft - 1};
  top: 0;
  left: ${sizes.appNavBarLeftWidth}px;
  height: 100vh;
  width: ${sizes.secondarySideBarWidth}px;
  padding: 0 16px 24px;
  background: ${color.backgroundLightest};
  border-right: 1px solid ${color.borderLightest};
  ${mixin.scrollableY}
  ${mixin.customScrollbar()}
  @media (max-width: 1100px) {
    width: ${sizes.secondarySideBarWidth - 10}px;
  }
  @media (max-width: 999px) {
    display: none;
  }
`;

export const ProjectInfo = styled.div`
  display: flex;
  padding: 24px 4px;
`;

export const ProjectTexts = styled.div`
  padding: 3px 0 0 10px;
`;

export const ProjectName = styled.div`
  color: ${color.textDark};
  ${font.size(15)};
  ${font.medium};
`;

export const ProjectCategory = styled.div`
  color: ${color.textMedium};
  ${font.size(13)};
`;

export const Divider = styled.div`
  margin-top: 17px;
  padding-top: 18px;
  border-top: 1px solid ${color.borderLight};
`;

export const LinkItem = styled.div`
  position: relative;
  display: flex;
  padding: 8px 12px;
  border-radius: 3px;
  ${mixin.clickable}
  ${props =>
    !props.to ? `cursor: not-allowed;` : `&:hover { background: ${color.backgroundLight}; }`}
  i {
    margin-right: 15px;
    font-size: 20px;
  }
  &.active {
    color: ${color.primary};
    background: ${color.backgroundLight};
    i {
      color: ${color.primary};
    }
  }
`;

export const LinkText = styled.div`
  padding-top: 2px;
  ${font.size(14.7)};
`;

export const NotImplemented = styled.div`
  display: inline-block;
  position: absolute;
  top: 7px;
  left: 40px;
  width: 140px;
  padding: 5px 0 5px 8px;
  border-radius: 3px;
  text-transform: uppercase;
  color: ${color.textDark};
  background: ${color.backgroundMedium};
  opacity: 0;
  ${font.size(11.5)};
  ${font.bold}
  ${LinkItem}:hover & {
    opacity: 1;
  }
`;


================================================
FILE: client/src/Project/Sidebar/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink, useRouteMatch } from 'react-router-dom';

import { ProjectCategoryCopy } from 'shared/constants/projects';
import { Icon, ProjectAvatar } from 'shared/components';

import {
  Sidebar,
  ProjectInfo,
  ProjectTexts,
  ProjectName,
  ProjectCategory,
  Divider,
  LinkItem,
  LinkText,
  NotImplemented,
} from './Styles';

const propTypes = {
  project: PropTypes.object.isRequired,
};

const ProjectSidebar = ({ project }) => {
  const match = useRouteMatch();

  return (
    <Sidebar>
      <ProjectInfo>
        <ProjectAvatar />
        <ProjectTexts>
          <ProjectName>{project.name}</ProjectName>
          <ProjectCategory>{ProjectCategoryCopy[project.category]} project</ProjectCategory>
        </ProjectTexts>
      </ProjectInfo>

      {renderLinkItem(match, 'Kanban Board', 'board', '/board')}
      {renderLinkItem(match, 'Project settings', 'settings', '/settings')}
      <Divider />
      {renderLinkItem(match, 'Releases', 'shipping')}
      {renderLinkItem(match, 'Issues and filters', 'issues')}
      {renderLinkItem(match, 'Pages', 'page')}
      {renderLinkItem(match, 'Reports', 'reports')}
      {renderLinkItem(match, 'Components', 'component')}
    </Sidebar>
  );
};

const renderLinkItem = (match, text, iconType, path) => {
  const isImplemented = !!path;

  const linkItemProps = isImplemented
    ? { as: NavLink, exact: true, to: `${match.path}${path}` }
    : { as: 'div' };

  return (
    <LinkItem {...linkItemProps}>
      <Icon type={iconType} />
      <LinkText>{text}</LinkText>
      {!isImplemented && <NotImplemented>Not implemented</NotImplemented>}
    </LinkItem>
  );
};

ProjectSidebar.propTypes = propTypes;

export default ProjectSidebar;


================================================
FILE: client/src/Project/Styles.js
================================================
import styled from 'styled-components';

import { sizes } from 'shared/utils/styles';

const paddingLeft = sizes.appNavBarLeftWidth + sizes.secondarySideBarWidth + 40;

export const ProjectPage = styled.div`
  padding: 25px 32px 50px ${paddingLeft}px;

  @media (max-width: 1100px) {
    padding: 25px 20px 50px ${paddingLeft - 20}px;
  }
  @media (max-width: 999px) {
    padding-left: ${paddingLeft - 20 - sizes.secondarySideBarWidth}px;
  }
`;


================================================
FILE: client/src/Project/index.jsx
================================================
import React from 'react';
import { Route, Redirect, useRouteMatch, useHistory } from 'react-router-dom';

import useApi from 'shared/hooks/api';
import { updateArrayItemById } from 'shared/utils/javascript';
import { createQueryParamModalHelpers } from 'shared/utils/queryParamModal';
import { PageLoader, PageError, Modal } from 'shared/components';

import NavbarLeft from './NavbarLeft';
import Sidebar from './Sidebar';
import Board from './Board';
import IssueSearch from './IssueSearch';
import IssueCreate from './IssueCreate';
import ProjectSettings from './ProjectSettings';
import { ProjectPage } from './Styles';

const Project = () => {
  const match = useRouteMatch();
  const history = useHistory();

  const issueSearchModalHelpers = createQueryParamModalHelpers('issue-search');
  const issueCreateModalHelpers = createQueryParamModalHelpers('issue-create');

  const [{ data, error, setLocalData }, fetchProject] = useApi.get('/project');

  if (!data) return <PageLoader />;
  if (error) return <PageError />;

  const { project } = data;

  const updateLocalProjectIssues = (issueId, updatedFields) => {
    setLocalData(currentData => ({
      project: {
        ...currentData.project,
        issues: updateArrayItemById(currentData.project.issues, issueId, updatedFields),
      },
    }));
  };

  return (
    <ProjectPage>
      <NavbarLeft
        issueSearchModalOpen={issueSearchModalHelpers.open}
        issueCreateModalOpen={issueCreateModalHelpers.open}
      />

      <Sidebar project={project} />

      {issueSearchModalHelpers.isOpen() && (
        <Modal
          isOpen
          testid="modal:issue-search"
          variant="aside"
          width={600}
          onClose={issueSearchModalHelpers.close}
          renderContent={() => <IssueSearch project={project} />}
        />
      )}

      {issueCreateModalHelpers.isOpen() && (
        <Modal
          isOpen
          testid="modal:issue-create"
          width={800}
          withCloseIcon={false}
          onClose={issueCreateModalHelpers.close}
          renderContent={modal => (
            <IssueCreate
              project={project}
              fetchProject={fetchProject}
              onCreate={() => history.push(`${match.url}/board`)}
              modalClo
Download .txt
gitextract_pfax6a_i/

├── .gitignore
├── .vscode/
│   └── settings.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── api/
│   ├── .eslintignore
│   ├── .eslintrc.json
│   ├── .prettierrc
│   ├── README.md
│   ├── package.json
│   ├── src/
│   │   ├── constants/
│   │   │   ├── issues.ts
│   │   │   └── projects.ts
│   │   ├── controllers/
│   │   │   ├── authentication.ts
│   │   │   ├── comments.ts
│   │   │   ├── issues.ts
│   │   │   ├── projects.ts
│   │   │   ├── test.ts
│   │   │   └── users.ts
│   │   ├── database/
│   │   │   ├── createConnection.ts
│   │   │   ├── createGuestAccount.ts
│   │   │   ├── createTestAccount.ts
│   │   │   └── resetDatabase.ts
│   │   ├── entities/
│   │   │   ├── Comment.ts
│   │   │   ├── Issue.ts
│   │   │   ├── Project.ts
│   │   │   ├── User.ts
│   │   │   └── index.ts
│   │   ├── errors/
│   │   │   ├── asyncCatch.ts
│   │   │   ├── customErrors.ts
│   │   │   └── index.ts
│   │   ├── index.ts
│   │   ├── middleware/
│   │   │   ├── authentication.ts
│   │   │   ├── errors.ts
│   │   │   └── response.ts
│   │   ├── routes.ts
│   │   ├── serializers/
│   │   │   └── issues.ts
│   │   ├── types/
│   │   │   ├── env.d.ts
│   │   │   └── express.d.ts
│   │   └── utils/
│   │       ├── authToken.ts
│   │       ├── typeorm.ts
│   │       └── validation.ts
│   ├── tsconfig-paths.js
│   └── tsconfig.json
├── client/
│   ├── .babelrc
│   ├── .eslintrc.json
│   ├── .prettierrc
│   ├── README.md
│   ├── cypress/
│   │   ├── .eslintrc.json
│   │   ├── integration/
│   │   │   ├── authentication.spec.js
│   │   │   ├── issueCreate.spec.js
│   │   │   ├── issueDetails.spec.js
│   │   │   ├── issueFilters.spec.js
│   │   │   ├── issueSearch.spec.js
│   │   │   ├── issuesDragDrop.spec.js
│   │   │   └── projectSettings.spec.js
│   │   ├── plugins/
│   │   │   └── index.js
│   │   └── support/
│   │       ├── commands.js
│   │       ├── index.js
│   │       └── utils.js
│   ├── cypress.json
│   ├── jest/
│   │   ├── fileMock.js
│   │   └── styleMock.js
│   ├── jest.config.js
│   ├── jsconfig.json
│   ├── package.json
│   ├── server.js
│   ├── src/
│   │   ├── App/
│   │   │   ├── BaseStyles.js
│   │   │   ├── NormalizeStyles.js
│   │   │   ├── Routes.jsx
│   │   │   ├── Toast/
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── assets/
│   │   │   │   └── fonts/
│   │   │   │       ├── CircularStd-Black.otf
│   │   │   │       ├── CircularStd-Bold.otf
│   │   │   │       ├── CircularStd-Book.otf
│   │   │   │       └── CircularStd-Medium.otf
│   │   │   ├── fontStyles.css
│   │   │   └── index.jsx
│   │   ├── Auth/
│   │   │   └── Authenticate.jsx
│   │   ├── Project/
│   │   │   ├── Board/
│   │   │   │   ├── Filters/
│   │   │   │   │   ├── Styles.js
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── Header/
│   │   │   │   │   ├── Styles.js
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── IssueDetails/
│   │   │   │   │   ├── AssigneesReporter/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Comments/
│   │   │   │   │   │   ├── BodyForm/
│   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   ├── Comment/
│   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   ├── Create/
│   │   │   │   │   │   │   ├── ProTip/
│   │   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Dates/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Delete.jsx
│   │   │   │   │   ├── Description/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── EstimateTracking/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   ├── TrackingWidget/
│   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Loader.jsx
│   │   │   │   │   ├── Priority/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Status/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Styles.js
│   │   │   │   │   ├── Title/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Type/
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── Lists/
│   │   │   │   │   ├── List/
│   │   │   │   │   │   ├── Issue/
│   │   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   ├── Styles.js
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Styles.js
│   │   │   │   │   └── index.jsx
│   │   │   │   └── index.jsx
│   │   │   ├── IssueCreate/
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── IssueSearch/
│   │   │   │   ├── NoResultsSvg.jsx
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── NavbarLeft/
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── ProjectSettings/
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── Sidebar/
│   │   │   │   ├── Styles.js
│   │   │   │   └── index.jsx
│   │   │   ├── Styles.js
│   │   │   └── index.jsx
│   │   ├── browserHistory.js
│   │   ├── index.html
│   │   ├── index.jsx
│   │   └── shared/
│   │       ├── components/
│   │       │   ├── AboutTooltip/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Avatar/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Breadcrumbs/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Button/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── ConfirmModal/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── CopyLinkButton.jsx
│   │       │   ├── DatePicker/
│   │       │   │   ├── DateSection.jsx
│   │       │   │   ├── Styles.js
│   │       │   │   ├── TimeSection.jsx
│   │       │   │   └── index.jsx
│   │       │   ├── Form/
│   │       │   │   ├── Field.jsx
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Icon/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Input/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── InputDebounced.jsx
│   │       │   ├── IssuePriorityIcon/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── IssueTypeIcon/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Logo.jsx
│   │       │   ├── Modal/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── PageError/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── PageLoader/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── ProjectAvatar.jsx
│   │       │   ├── Select/
│   │       │   │   ├── Dropdown.jsx
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Spinner.jsx
│   │       │   ├── TextEditedContent/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── TextEditor/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Textarea/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   ├── Tooltip/
│   │       │   │   ├── Styles.js
│   │       │   │   └── index.jsx
│   │       │   └── index.js
│   │       ├── constants/
│   │       │   ├── issues.js
│   │       │   ├── keyCodes.js
│   │       │   └── projects.js
│   │       ├── hooks/
│   │       │   ├── api/
│   │       │   │   ├── index.js
│   │       │   │   ├── mutation.js
│   │       │   │   └── query.js
│   │       │   ├── currentUser.js
│   │       │   ├── deepCompareMemoize.js
│   │       │   ├── mergeState.js
│   │       │   ├── onEscapeKeyDown.js
│   │       │   └── onOutsideClick.js
│   │       └── utils/
│   │           ├── api.js
│   │           ├── authToken.js
│   │           ├── browser.js
│   │           ├── dateTime.js
│   │           ├── javascript.js
│   │           ├── queryParamModal.js
│   │           ├── styles.js
│   │           ├── toast.js
│   │           ├── url.js
│   │           └── validation.js
│   ├── webpack.config.js
│   └── webpack.config.production.js
└── package.json
Download .txt
SYMBOL INDEX (30 symbols across 11 files)

FILE: api/src/constants/issues.ts
  type IssueType (line 1) | enum IssueType {
  type IssueStatus (line 7) | enum IssueStatus {
  type IssuePriority (line 14) | enum IssuePriority {

FILE: api/src/constants/projects.ts
  type ProjectCategory (line 1) | enum ProjectCategory {

FILE: api/src/entities/Comment.ts
  class Comment (line 14) | @Entity()

FILE: api/src/entities/Issue.ts
  class Issue (line 22) | @Entity()

FILE: api/src/entities/Project.ts
  class Project (line 15) | @Entity()

FILE: api/src/entities/User.ts
  class User (line 17) | @Entity()

FILE: api/src/errors/customErrors.ts
  type ErrorData (line 3) | type ErrorData = { [key: string]: any };
  class CustomError (line 5) | class CustomError extends Error {
    method constructor (line 6) | constructor(
  class RouteNotFoundError (line 16) | class RouteNotFoundError extends CustomError {
    method constructor (line 17) | constructor(originalUrl: string) {
  class EntityNotFoundError (line 22) | class EntityNotFoundError extends CustomError {
    method constructor (line 23) | constructor(entityName: string) {
  class BadUserInputError (line 28) | class BadUserInputError extends CustomError {
    method constructor (line 29) | constructor(errorData: ErrorData) {
  class InvalidTokenError (line 34) | class InvalidTokenError extends CustomError {
    method constructor (line 35) | constructor(message = 'Authentication token is invalid.') {

FILE: api/src/types/env.d.ts
  type ProcessEnv (line 2) | interface ProcessEnv {

FILE: api/src/types/express.d.ts
  type Response (line 2) | interface Response {
  type Request (line 5) | interface Request {

FILE: api/src/utils/typeorm.ts
  type EntityConstructor (line 7) | type EntityConstructor = typeof Project | typeof User | typeof Issue | t...
  type EntityInstance (line 8) | type EntityInstance = Project | User | Issue | Comment;

FILE: api/src/utils/validation.ts
  type Value (line 1) | type Value = any;
  type ErrorMessage (line 2) | type ErrorMessage = false | string;
  type FieldValues (line 3) | type FieldValues = { [key: string]: Value };
  type Validator (line 4) | type Validator = (value: Value, fieldValues?: FieldValues) => ErrorMessage;
  type FieldValidators (line 5) | type FieldValidators = { [key: string]: Validator | Validator[] };
  type FieldErrors (line 6) | type FieldErrors = { [key: string]: string };
Condensed preview — 209 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (308K chars).
[
  {
    "path": ".gitignore",
    "chars": 123,
    "preview": "# dependencies\n**/node_modules\n\n# misc\n**/.DS_Store\n\n# environment config\n**/.env\n\n# production\n**/build\n**/npm-debug.lo"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 187,
    "preview": "{\n  \"eslint.workingDirectories\": [\n    {\n      \"directory\": \"./client\",\n      \"changeProcessCWD\": true\n    },\n    {\n    "
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 98,
    "preview": "I will not be accepting PR's on this repository. Feel free to fork and maintain your own version.\n"
  },
  {
    "path": "LICENSE",
    "chars": 1091,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2013-present, Yuxi (Evan) You\n\nPermission is hereby granted, free of charge, to any"
  },
  {
    "path": "README.md",
    "chars": 4230,
    "preview": "<h1 align=\"center\">A simplified Jira clone built with React and Node</h1>\n\n<div align=\"center\">Auto formatted with Prett"
  },
  {
    "path": "api/.eslintignore",
    "chars": 26,
    "preview": "build/*\ntsconfig-paths.js\n"
  },
  {
    "path": "api/.eslintrc.json",
    "chars": 1007,
    "preview": "{\n  \"parser\": \"@typescript-eslint/parser\",\n  \"parserOptions\": {\n    \"project\": \"./tsconfig.json\",\n    \"sourceType\": \"mod"
  },
  {
    "path": "api/.prettierrc",
    "chars": 73,
    "preview": "{\n  \"printWidth\": 100,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "api/README.md",
    "chars": 3033,
    "preview": "# Project structure 🏗\n\nThe API codebase is fairly simple and should be easy enough to understand.\n\n<br>\n\n| File or folde"
  },
  {
    "path": "api/package.json",
    "chars": 1656,
    "preview": "{\n  \"name\": \"jira_api\",\n  \"version\": \"1.0.0\",\n  \"author\": \"Ivor Reic\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \""
  },
  {
    "path": "api/src/constants/issues.ts",
    "chars": 307,
    "preview": "export enum IssueType {\n  TASK = 'task',\n  BUG = 'bug',\n  STORY = 'story',\n}\n\nexport enum IssueStatus {\n  BACKLOG = 'bac"
  },
  {
    "path": "api/src/constants/projects.ts",
    "chars": 109,
    "preview": "export enum ProjectCategory {\n  SOFTWARE = 'software',\n  MARKETING = 'marketing',\n  BUSINESS = 'business',\n}\n"
  },
  {
    "path": "api/src/controllers/authentication.ts",
    "chars": 318,
    "preview": "import { catchErrors } from 'errors';\nimport { signToken } from 'utils/authToken';\nimport createAccount from 'database/c"
  },
  {
    "path": "api/src/controllers/comments.ts",
    "chars": 620,
    "preview": "import { Comment } from 'entities';\nimport { catchErrors } from 'errors';\nimport { updateEntity, deleteEntity, createEnt"
  },
  {
    "path": "api/src/controllers/issues.ts",
    "chars": 1760,
    "preview": "import { Issue } from 'entities';\nimport { catchErrors } from 'errors';\nimport { updateEntity, deleteEntity, createEntit"
  },
  {
    "path": "api/src/controllers/projects.ts",
    "chars": 678,
    "preview": "import { Project } from 'entities';\nimport { catchErrors } from 'errors';\nimport { findEntityOrThrow, updateEntity } fro"
  },
  {
    "path": "api/src/controllers/test.ts",
    "chars": 495,
    "preview": "import { catchErrors } from 'errors';\nimport { signToken } from 'utils/authToken';\nimport resetTestDatabase from 'databa"
  },
  {
    "path": "api/src/controllers/users.ts",
    "chars": 150,
    "preview": "import { catchErrors } from 'errors';\n\nexport const getCurrentUser = catchErrors((req, res) => {\n  res.respond({ current"
  },
  {
    "path": "api/src/database/createConnection.ts",
    "chars": 496,
    "preview": "import { createConnection, Connection } from 'typeorm';\n\nimport * as entities from 'entities';\n\nconst createDatabaseConn"
  },
  {
    "path": "api/src/database/createGuestAccount.ts",
    "chars": 14683,
    "preview": "import { Comment, Issue, Project, User } from 'entities';\nimport { ProjectCategory } from 'constants/projects';\nimport {"
  },
  {
    "path": "api/src/database/createTestAccount.ts",
    "chars": 2351,
    "preview": "import { Comment, Issue, Project, User } from 'entities';\nimport { ProjectCategory } from 'constants/projects';\nimport {"
  },
  {
    "path": "api/src/database/resetDatabase.ts",
    "chars": 234,
    "preview": "import { getConnection } from 'typeorm';\n\nconst resetDatabase = async (): Promise<void> => {\n  const connection = getCon"
  },
  {
    "path": "api/src/entities/Comment.ts",
    "chars": 831,
    "preview": "import {\n  BaseEntity,\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  UpdateDateColumn,\n  ManyToOne"
  },
  {
    "path": "api/src/entities/Issue.ts",
    "chars": 2177,
    "preview": "import striptags from 'striptags';\nimport {\n  BaseEntity,\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  CreateDateColu"
  },
  {
    "path": "api/src/entities/Project.ts",
    "chars": 1056,
    "preview": "import {\n  BaseEntity,\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  UpdateDateColumn,\n  OneToMany"
  },
  {
    "path": "api/src/entities/User.ts",
    "chars": 1097,
    "preview": "import {\n  BaseEntity,\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  UpdateDateColumn,\n  OneToMany"
  },
  {
    "path": "api/src/entities/index.ts",
    "chars": 182,
    "preview": "export { default as Comment } from './Comment';\nexport { default as Issue } from './Issue';\nexport { default as Project "
  },
  {
    "path": "api/src/errors/asyncCatch.ts",
    "chars": 291,
    "preview": "import { RequestHandler } from 'express';\n\nexport const catchErrors = (requestHandler: RequestHandler): RequestHandler ="
  },
  {
    "path": "api/src/errors/customErrors.ts",
    "chars": 990,
    "preview": "/* eslint-disable max-classes-per-file */\n\ntype ErrorData = { [key: string]: any };\n\nexport class CustomError extends Er"
  },
  {
    "path": "api/src/errors/index.ts",
    "chars": 76,
    "preview": "export * from './customErrors';\nexport { catchErrors } from './asyncCatch';\n"
  },
  {
    "path": "api/src/index.ts",
    "chars": 1209,
    "preview": "import 'module-alias/register';\nimport 'dotenv/config';\nimport 'reflect-metadata';\nimport express from 'express';\nimport"
  },
  {
    "path": "api/src/middleware/authentication.ts",
    "chars": 918,
    "preview": "import { Request } from 'express';\n\nimport { verifyToken } from 'utils/authToken';\nimport { catchErrors, InvalidTokenErr"
  },
  {
    "path": "api/src/middleware/errors.ts",
    "chars": 609,
    "preview": "import { ErrorRequestHandler } from 'express';\nimport { pick } from 'lodash';\n\nimport { CustomError } from 'errors';\n\nex"
  },
  {
    "path": "api/src/middleware/response.ts",
    "chars": 202,
    "preview": "import { RequestHandler } from 'express';\n\nexport const addRespondToResponse: RequestHandler = (_req, res, next) => {\n  "
  },
  {
    "path": "api/src/routes.ts",
    "chars": 1202,
    "preview": "import * as authentication from 'controllers/authentication';\nimport * as comments from 'controllers/comments';\nimport *"
  },
  {
    "path": "api/src/serializers/issues.ts",
    "chars": 285,
    "preview": "import { pick } from 'lodash';\n\nimport { Issue } from 'entities';\n\nexport const issuePartial = (issue: Issue): Partial<I"
  },
  {
    "path": "api/src/types/env.d.ts",
    "chars": 206,
    "preview": "declare namespace NodeJS {\n  export interface ProcessEnv {\n    DB_HOST: string;\n    DB_PORT: string;\n    DB_USERNAME: st"
  },
  {
    "path": "api/src/types/express.d.ts",
    "chars": 173,
    "preview": "declare namespace Express {\n  export interface Response {\n    respond: (data: any) => void;\n  }\n  export interface Reque"
  },
  {
    "path": "api/src/utils/authToken.ts",
    "chars": 625,
    "preview": "import jwt, { SignOptions } from 'jsonwebtoken';\nimport { isPlainObject } from 'lodash';\n\nimport { InvalidTokenError } f"
  },
  {
    "path": "api/src/utils/typeorm.ts",
    "chars": 2072,
    "preview": "import { FindOneOptions } from 'typeorm/find-options/FindOneOptions';\n\nimport { Project, User, Issue, Comment } from 'en"
  },
  {
    "path": "api/src/utils/validation.ts",
    "chars": 2115,
    "preview": "type Value = any;\ntype ErrorMessage = false | string;\ntype FieldValues = { [key: string]: Value };\ntype Validator = (val"
  },
  {
    "path": "api/tsconfig-paths.js",
    "chars": 479,
    "preview": "const tsConfigPaths = require('tsconfig-paths');\n\nconst tsConfig = require('./tsconfig.json');\n\n// Typescript compiler d"
  },
  {
    "path": "api/tsconfig.json",
    "chars": 965,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es6\",\n    \"module\": \"commonjs\",\n    \"lib\": [\"dom\", \"es6\", \"es2017\", \"es2019\", \"e"
  },
  {
    "path": "client/.babelrc",
    "chars": 395,
    "preview": "{\n  \"presets\": [\n    [\n      \"@babel/preset-env\",\n      {\n        \"useBuiltIns\": \"entry\",\n        \"corejs\": 3\n      }\n  "
  },
  {
    "path": "client/.eslintrc.json",
    "chars": 1189,
    "preview": "{\n  \"parser\": \"babel-eslint\",\n  \"parserOptions\": {\n    \"sourceType\": \"module\",\n    \"ecmaVersion\": 8,\n    \"ecmaFeatures\":"
  },
  {
    "path": "client/.prettierrc",
    "chars": 73,
    "preview": "{\n  \"printWidth\": 100,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "client/README.md",
    "chars": 2406,
    "preview": "# Project structure 🏗\n\nI've used this architecture on multiple larger projects in the past and it performed really well."
  },
  {
    "path": "client/cypress/.eslintrc.json",
    "chars": 134,
    "preview": "{\n  \"extends\": [\"plugin:cypress/recommended\"],\n  \"rules\": {\n    \"no-unused-expressions\": 0 // chai assertions trigger th"
  },
  {
    "path": "client/cypress/integration/authentication.spec.js",
    "chars": 480,
    "preview": "import { testid } from '../support/utils';\n\ndescribe('Authentication', () => {\n  beforeEach(() => {\n    cy.resetDatabase"
  },
  {
    "path": "client/cypress/integration/issueCreate.spec.js",
    "chars": 1272,
    "preview": "import { testid } from '../support/utils';\n\ndescribe('Issue create', () => {\n  beforeEach(() => {\n    cy.resetDatabase()"
  },
  {
    "path": "client/cypress/integration/issueDetails.spec.js",
    "chars": 4907,
    "preview": "import { testid } from '../support/utils';\n\ndescribe('Issue details', () => {\n  beforeEach(() => {\n    cy.resetDatabase("
  },
  {
    "path": "client/cypress/integration/issueFilters.spec.js",
    "chars": 1106,
    "preview": "import { testid } from '../support/utils';\n\ndescribe('Issue filters', () => {\n  beforeEach(() => {\n    cy.resetDatabase("
  },
  {
    "path": "client/cypress/integration/issueSearch.spec.js",
    "chars": 1571,
    "preview": "import { testid } from '../support/utils';\n\ndescribe('Issue search', () => {\n  beforeEach(() => {\n    cy.resetDatabase()"
  },
  {
    "path": "client/cypress/integration/issuesDragDrop.spec.js",
    "chars": 1605,
    "preview": "import { KeyCodes } from 'shared/constants/keyCodes';\n\nimport { testid } from '../support/utils';\n\ndescribe('Issues drag"
  },
  {
    "path": "client/cypress/integration/projectSettings.spec.js",
    "chars": 1212,
    "preview": "import { testid } from '../support/utils';\n\ndescribe('Project settings', () => {\n  beforeEach(() => {\n    cy.resetDataba"
  },
  {
    "path": "client/cypress/plugins/index.js",
    "chars": 796,
    "preview": "/* eslint-disable global-require */\n/* eslint-disable import/no-extraneous-dependencies */\n\n// *************************"
  },
  {
    "path": "client/cypress/support/commands.js",
    "chars": 2640,
    "preview": "import 'core-js/stable';\nimport 'regenerator-runtime/runtime';\n\nimport '@4tw/cypress-drag-drop';\n\nimport { objectToQuery"
  },
  {
    "path": "client/cypress/support/index.js",
    "chars": 556,
    "preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
  },
  {
    "path": "client/cypress/support/utils.js",
    "chars": 165,
    "preview": "export const testid = (strings, ...values) => {\n  const id = strings.map((str, index) => str + (values[index] || '')).jo"
  },
  {
    "path": "client/cypress.json",
    "chars": 149,
    "preview": "{\n  \"baseUrl\": \"http://localhost:8080\",\n  \"viewportHeight\": 800,\n  \"viewportWidth\": 1440,\n  \"env\": {\n    \"apiBaseUrl\": \""
  },
  {
    "path": "client/jest/fileMock.js",
    "chars": 35,
    "preview": "module.exports = 'test-file-stub';\n"
  },
  {
    "path": "client/jest/styleMock.js",
    "chars": 21,
    "preview": "module.exports = {};\n"
  },
  {
    "path": "client/jest.config.js",
    "chars": 324,
    "preview": "module.exports = {\n  moduleFileExtensions: ['*', 'js', 'jsx'],\n  moduleDirectories: ['src', 'node_modules'],\n  moduleNam"
  },
  {
    "path": "client/jsconfig.json",
    "chars": 238,
    "preview": "// This config allows VSCode intellisense to work with absolute \"src\" imports and jsx files\n{\n  \"compilerOptions\": {\n   "
  },
  {
    "path": "client/package.json",
    "chars": 2437,
    "preview": "{\n  \"name\": \"jira_client\",\n  \"version\": \"1.0.0\",\n  \"author\": \"Ivor Reic\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\""
  },
  {
    "path": "client/server.js",
    "chars": 327,
    "preview": "const express = require('express');\nconst fallback = require('express-history-api-fallback');\nconst compression = requir"
  },
  {
    "path": "client/src/App/BaseStyles.js",
    "chars": 1764,
    "preview": "import { createGlobalStyle } from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\n\nexpor"
  },
  {
    "path": "client/src/App/NormalizeStyles.js",
    "chars": 2636,
    "preview": "import { createGlobalStyle } from 'styled-components';\n\n/** DO NOT ALTER THIS FILE. It is a copy of https://necolas.gith"
  },
  {
    "path": "client/src/App/Routes.jsx",
    "chars": 583,
    "preview": "import React from 'react';\nimport { Router, Switch, Route, Redirect } from 'react-router-dom';\n\nimport history from 'bro"
  },
  {
    "path": "client/src/App/Toast/Styles.js",
    "chars": 1158,
    "preview": "import styled from 'styled-components';\n\nimport { color, font, mixin, zIndexValues } from 'shared/utils/styles';\nimport "
  },
  {
    "path": "client/src/App/Toast/index.jsx",
    "chars": 1453,
    "preview": "import React, { useState, useEffect } from 'react';\nimport { CSSTransition, TransitionGroup } from 'react-transition-gro"
  },
  {
    "path": "client/src/App/fontStyles.css",
    "chars": 1161,
    "preview": "@font-face {\n  font-family: 'CircularStdBlack';\n  src: url('./assets/fonts/CircularStd-Black.woff2') format('woff2'),\n  "
  },
  {
    "path": "client/src/App/index.jsx",
    "chars": 593,
    "preview": "import React, { Fragment } from 'react';\n\nimport NormalizeStyles from './NormalizeStyles';\nimport BaseStyles from './Bas"
  },
  {
    "path": "client/src/Auth/Authenticate.jsx",
    "chars": 775,
    "preview": "import React, { useEffect } from 'react';\nimport { useHistory } from 'react-router-dom';\n\nimport api from 'shared/utils/"
  },
  {
    "path": "client/src/Project/Board/Filters/Styles.js",
    "chars": 1197,
    "preview": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport { InputDebounc"
  },
  {
    "path": "client/src/Project/Board/Filters/index.jsx",
    "chars": 1844,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { xor } from 'lodash';\n\nimport {\n  Filters,\n  Sear"
  },
  {
    "path": "client/src/Project/Board/Header/Styles.js",
    "chars": 270,
    "preview": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\n\nexport const Header = styled.div`\n"
  },
  {
    "path": "client/src/Project/Board/Header/index.jsx",
    "chars": 411,
    "preview": "import React from 'react';\n\nimport { Button } from 'shared/components';\n\nimport { Header, BoardName } from './Styles';\n\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/AssigneesReporter/Styles.js",
    "chars": 606,
    "preview": "import styled, { css } from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\n\nexport cons"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/AssigneesReporter/index.jsx",
    "chars": 2273,
    "preview": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Avatar, Select, Icon } from 'shar"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/BodyForm/Styles.js",
    "chars": 229,
    "preview": "import styled from 'styled-components';\n\nimport { Button } from 'shared/components';\n\nexport const Actions = styled.div`"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/BodyForm/index.jsx",
    "chars": 1240,
    "preview": "import React, { Fragment, useRef } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Textarea } from 'shared/c"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Comment/Styles.js",
    "chars": 1228,
    "preview": "import styled, { css } from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport { Ava"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Comment/index.jsx",
    "chars": 2437,
    "preview": "import React, { Fragment, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport api from 'shared/utils/ap"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Create/ProTip/Styles.js",
    "chars": 539,
    "preview": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const Tip = styled.d"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Create/ProTip/index.jsx",
    "chars": 1000,
    "preview": "import React, { useEffect } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { KeyCodes } from 'shared/constant"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Create/Styles.js",
    "chars": 633,
    "preview": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport { Avatar } fro"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Create/index.jsx",
    "chars": 1783,
    "preview": "import React, { Fragment, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport api from 'shared/utils/ap"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Styles.js",
    "chars": 219,
    "preview": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\n\nexport const Comments = styled.div"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/index.jsx",
    "chars": 776,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { sortByNewest } from 'shared/utils/javascript';\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Dates/Styles.js",
    "chars": 289,
    "preview": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const Dates = styled"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Dates/index.jsx",
    "chars": 560,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { formatDateTimeConversational } from 'shared/uti"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Delete.jsx",
    "chars": 1082,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport api from 'shared/utils/api';\nimport toast from 's"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Description/Styles.js",
    "chars": 573,
    "preview": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\n\nexport const Title ="
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Description/index.jsx",
    "chars": 1751,
    "preview": "import React, { Fragment, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { getTextContentsFromHtml"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/EstimateTracking/Styles.js",
    "chars": 846,
    "preview": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\n\nexport const Trackin"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/EstimateTracking/TrackingWidget/Styles.js",
    "chars": 789,
    "preview": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\nimport { Icon } from 'shared"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/EstimateTracking/TrackingWidget/index.jsx",
    "chars": 1482,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { isNil } from 'lodash';\n\nimport { TrackingWidget,"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/EstimateTracking/index.jsx",
    "chars": 2167,
    "preview": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\nimport { isNil } from 'lodash';\n\nimport { I"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Loader.jsx",
    "chars": 1269,
    "preview": "import React from 'react';\nimport ContentLoader from 'react-content-loader';\n\nconst IssueDetailsLoader = () => (\n  <div "
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Priority/Styles.js",
    "chars": 456,
    "preview": "import styled, { css } from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const Prior"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Priority/index.jsx",
    "chars": 1355,
    "preview": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { IssuePriority, IssuePriorityCopy "
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Status/Styles.js",
    "chars": 481,
    "preview": "import styled, { css } from 'styled-components';\n\nimport { issueStatusColors, issueStatusBackgroundColors, mixin } from "
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Status/index.jsx",
    "chars": 1279,
    "preview": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { IssueStatus, IssueStatusCopy } fr"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Styles.js",
    "chars": 717,
    "preview": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const Content = styl"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Title/Styles.js",
    "chars": 715,
    "preview": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\nimport { Textarea } from 'sh"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Title/index.jsx",
    "chars": 1387,
    "preview": "import React, { Fragment, useRef, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { KeyCodes } from"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Type/Styles.js",
    "chars": 445,
    "preview": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\nimport { Button } from 'shar"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Type/index.jsx",
    "chars": 1216,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { IssueType, IssueTypeCopy } from 'shared/constan"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/index.jsx",
    "chars": 3021,
    "preview": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\n\nimport api from 'shared/utils/api';\nimport"
  },
  {
    "path": "client/src/Project/Board/Lists/List/Issue/Styles.js",
    "chars": 1198,
    "preview": "import styled, { css } from 'styled-components';\nimport { Link } from 'react-router-dom';\n\nimport { color, font, mixin }"
  },
  {
    "path": "client/src/Project/Board/Lists/List/Issue/index.jsx",
    "chars": 1818,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { useRouteMatch } from 'react-router-dom';\nimport "
  },
  {
    "path": "client/src/Project/Board/Lists/List/Styles.js",
    "chars": 629,
    "preview": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\n\nexport const List = "
  },
  {
    "path": "client/src/Project/Board/Lists/List/index.jsx",
    "chars": 2716,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport moment from 'moment';\nimport { Droppable } from 'r"
  },
  {
    "path": "client/src/Project/Board/Lists/Styles.js",
    "chars": 117,
    "preview": "import styled from 'styled-components';\n\nexport const Lists = styled.div`\n  display: flex;\n  margin: 26px -5px 0;\n`;\n"
  },
  {
    "path": "client/src/Project/Board/Lists/index.jsx",
    "chars": 3270,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { DragDropContext } from 'react-beautiful-dnd';\n\ni"
  },
  {
    "path": "client/src/Project/Board/index.jsx",
    "chars": 2035,
    "preview": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\nimport { Route, useRouteMatch, useHistory }"
  },
  {
    "path": "client/src/Project/IssueCreate/Styles.js",
    "chars": 828,
    "preview": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\nimport { Button, Form } from"
  },
  {
    "path": "client/src/Project/IssueCreate/index.jsx",
    "chars": 4965,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport {\n  IssueType,\n  IssueStatus,\n  IssuePriority,\n  "
  },
  {
    "path": "client/src/Project/IssueSearch/NoResultsSvg.jsx",
    "chars": 22927,
    "preview": "import React from 'react';\n\nconst NoResults = () => (\n  <svg\n    width=\"160px\"\n    height=\"146px\"\n    viewBox=\"0 0 160 1"
  },
  {
    "path": "client/src/Project/IssueSearch/Styles.js",
    "chars": 1885,
    "preview": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport { InputDebounc"
  },
  {
    "path": "client/src/Project/IssueSearch/index.jsx",
    "chars": 2671,
    "preview": "import React, { Fragment, useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { Link } from 'react-route"
  },
  {
    "path": "client/src/Project/NavbarLeft/Styles.js",
    "chars": 1594,
    "preview": "import styled from 'styled-components';\nimport { NavLink } from 'react-router-dom';\n\nimport { font, sizes, color, mixin,"
  },
  {
    "path": "client/src/Project/NavbarLeft/index.jsx",
    "chars": 1163,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Icon, AboutTooltip } from 'shared/components';\n"
  },
  {
    "path": "client/src/Project/ProjectSettings/Styles.js",
    "chars": 476,
    "preview": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\nimport { Button, Form } from 'share"
  },
  {
    "path": "client/src/Project/ProjectSettings/index.jsx",
    "chars": 2221,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { ProjectCategory, ProjectCategoryCopy } from 'sh"
  },
  {
    "path": "client/src/Project/Sidebar/Styles.js",
    "chars": 2013,
    "preview": "import styled from 'styled-components';\n\nimport { color, sizes, font, mixin, zIndexValues } from 'shared/utils/styles';\n"
  },
  {
    "path": "client/src/Project/Sidebar/index.jsx",
    "chars": 1777,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { NavLink, useRouteMatch } from 'react-router-dom'"
  },
  {
    "path": "client/src/Project/Styles.js",
    "chars": 447,
    "preview": "import styled from 'styled-components';\n\nimport { sizes } from 'shared/utils/styles';\n\nconst paddingLeft = sizes.appNavB"
  },
  {
    "path": "client/src/Project/index.jsx",
    "chars": 2870,
    "preview": "import React from 'react';\nimport { Route, Redirect, useRouteMatch, useHistory } from 'react-router-dom';\n\nimport useApi"
  },
  {
    "path": "client/src/browserHistory.js",
    "chars": 88,
    "preview": "import { createBrowserHistory } from 'history';\n\nexport default createBrowserHistory();\n"
  },
  {
    "path": "client/src/index.html",
    "chars": 242,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initia"
  },
  {
    "path": "client/src/index.jsx",
    "chars": 209,
    "preview": "import 'core-js/stable';\nimport 'regenerator-runtime/runtime';\n\nimport React from 'react';\nimport ReactDOM from 'react-d"
  },
  {
    "path": "client/src/shared/components/AboutTooltip/Styles.js",
    "chars": 433,
    "preview": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\n\nexport const FeedbackDropdown = st"
  },
  {
    "path": "client/src/shared/components/AboutTooltip/index.jsx",
    "chars": 1383,
    "preview": "import React from 'react';\n\nimport Button from 'shared/components/Button';\nimport Tooltip from 'shared/components/Toolti"
  },
  {
    "path": "client/src/shared/components/Avatar/Styles.js",
    "chars": 720,
    "preview": "import styled from 'styled-components';\n\nimport { font, mixin } from 'shared/utils/styles';\n\nexport const Image = styled"
  },
  {
    "path": "client/src/shared/components/Avatar/index.jsx",
    "chars": 1071,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Image, Letter } from './Styles';\n\nconst propTyp"
  },
  {
    "path": "client/src/shared/components/Breadcrumbs/Styles.js",
    "chars": 295,
    "preview": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const Container = st"
  },
  {
    "path": "client/src/shared/components/Breadcrumbs/index.jsx",
    "chars": 470,
    "preview": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Container, Divider } from './Styl"
  },
  {
    "path": "client/src/shared/components/Button/Styles.js",
    "chars": 1940,
    "preview": "import styled, { css } from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport Spinn"
  },
  {
    "path": "client/src/shared/components/Button/index.jsx",
    "chars": 1808,
    "preview": "import React, { forwardRef } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { color } from 'shared/utils/styl"
  },
  {
    "path": "client/src/shared/components/ConfirmModal/Styles.js",
    "chars": 625,
    "preview": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\nimport Modal from 'shared/component"
  },
  {
    "path": "client/src/shared/components/ConfirmModal/index.jsx",
    "chars": 1888,
    "preview": "import React, { Fragment, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { StyledConfirmModal, Tit"
  },
  {
    "path": "client/src/shared/components/CopyLinkButton.jsx",
    "chars": 590,
    "preview": "import React, { useState } from 'react';\n\nimport { copyToClipboard } from 'shared/utils/browser';\nimport { Button } from"
  },
  {
    "path": "client/src/shared/components/DatePicker/DateSection.jsx",
    "chars": 3728,
    "preview": "import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\nimport moment from 'moment';\nimport { times"
  },
  {
    "path": "client/src/shared/components/DatePicker/Styles.js",
    "chars": 2202,
    "preview": "import styled, { css } from 'styled-components';\n\nimport { color, font, mixin, zIndexValues } from 'shared/utils/styles'"
  },
  {
    "path": "client/src/shared/components/DatePicker/TimeSection.jsx",
    "chars": 2002,
    "preview": "import React, { useLayoutEffect, useRef } from 'react';\nimport PropTypes from 'prop-types';\nimport moment from 'moment';"
  },
  {
    "path": "client/src/shared/components/DatePicker/index.jsx",
    "chars": 1863,
    "preview": "import React, { useState, useRef } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { formatDate, formatDateTim"
  },
  {
    "path": "client/src/shared/components/Form/Field.jsx",
    "chars": 1655,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { uniqueId } from 'lodash';\n\nimport Input from 'sh"
  },
  {
    "path": "client/src/shared/components/Form/Styles.js",
    "chars": 560,
    "preview": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const StyledField = "
  },
  {
    "path": "client/src/shared/components/Form/index.jsx",
    "chars": 1776,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Formik, Form as FormikForm, Field as FormikField"
  },
  {
    "path": "client/src/shared/components/Icon/Styles.js",
    "chars": 566,
    "preview": "import styled from 'styled-components';\n\nexport const StyledIcon = styled.i`\n  display: inline-block;\n  font-size: ${pro"
  },
  {
    "path": "client/src/shared/components/Icon/index.jsx",
    "chars": 1442,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { StyledIcon } from './Styles';\n\nconst fontIconCo"
  },
  {
    "path": "client/src/shared/components/Input/Styles.js",
    "chars": 1117,
    "preview": "import styled, { css } from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\nimport Icon from 's"
  },
  {
    "path": "client/src/shared/components/Input/index.jsx",
    "chars": 1085,
    "preview": "import React, { forwardRef } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { StyledInput, InputElement, Styl"
  },
  {
    "path": "client/src/shared/components/InputDebounced.jsx",
    "chars": 1146,
    "preview": "import React, { useState, useRef, useEffect, useCallback } from 'react';\nimport PropTypes from 'prop-types';\nimport { de"
  },
  {
    "path": "client/src/shared/components/IssuePriorityIcon/Styles.js",
    "chars": 243,
    "preview": "import styled from 'styled-components';\n\nimport { issuePriorityColors } from 'shared/utils/styles';\nimport { Icon } from"
  },
  {
    "path": "client/src/shared/components/IssuePriorityIcon/index.jsx",
    "chars": 574,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { IssuePriority } from 'shared/constants/issues';"
  },
  {
    "path": "client/src/shared/components/IssueTypeIcon/Styles.js",
    "chars": 231,
    "preview": "import styled from 'styled-components';\n\nimport { issueTypeColors } from 'shared/utils/styles';\nimport { Icon } from 'sh"
  },
  {
    "path": "client/src/shared/components/IssueTypeIcon/index.jsx",
    "chars": 354,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { TypeIcon } from './Styles';\n\nconst propTypes = "
  },
  {
    "path": "client/src/shared/components/Logo.jsx",
    "chars": 1766,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nconst propTypes = {\n  className: PropTypes.string,\n  siz"
  },
  {
    "path": "client/src/shared/components/Modal/Styles.js",
    "chars": 1822,
    "preview": "import styled, { css } from 'styled-components';\n\nimport { color, mixin, zIndexValues } from 'shared/utils/styles';\nimpo"
  },
  {
    "path": "client/src/shared/components/Modal/index.jsx",
    "chars": 2641,
    "preview": "import React, { Fragment, useState, useRef, useEffect, useCallback } from 'react';\nimport ReactDOM from 'react-dom';\nimp"
  },
  {
    "path": "client/src/shared/components/PageError/Styles.js",
    "chars": 971,
    "preview": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport { Icon } from "
  },
  {
    "path": "client/src/shared/components/PageError/index.jsx",
    "chars": 595,
    "preview": "import React from 'react';\n\nimport { ErrorPage, ErrorPageInner, ErrorBox, StyledIcon, Title } from './Styles';\n\nconst Pa"
  },
  {
    "path": "client/src/shared/components/PageLoader/Styles.js",
    "chars": 128,
    "preview": "import styled from 'styled-components';\n\nexport default styled.div`\n  width: 100%;\n  padding: 200px 0;\n  text-align: cen"
  },
  {
    "path": "client/src/shared/components/PageLoader/index.jsx",
    "chars": 247,
    "preview": "import React from 'react';\n\nimport Spinner from 'shared/components/Spinner';\n\nimport StyledPageLoader from './Styles';\n\n"
  },
  {
    "path": "client/src/shared/components/ProjectAvatar.jsx",
    "chars": 5321,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nconst propTypes = {\n  className: PropTypes.string,\n  siz"
  },
  {
    "path": "client/src/shared/components/Select/Dropdown.jsx",
    "chars": 7055,
    "preview": "import React, { useState, useRef, useLayoutEffect } from 'react';\nimport PropTypes from 'prop-types';\nimport { uniq } fr"
  },
  {
    "path": "client/src/shared/components/Select/Styles.js",
    "chars": 3025,
    "preview": "import styled, { css } from 'styled-components';\n\nimport { color, font, mixin, zIndexValues } from 'shared/utils/styles'"
  },
  {
    "path": "client/src/shared/components/Select/index.jsx",
    "chars": 5588,
    "preview": "import React, { useState, useRef } from 'react';\nimport PropTypes from 'prop-types';\n\nimport useOnOutsideClick from 'sha"
  },
  {
    "path": "client/src/shared/components/Spinner.jsx",
    "chars": 5858,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { color as colors } from 'shared/utils/styles';\n\n"
  },
  {
    "path": "client/src/shared/components/TextEditedContent/Styles.js",
    "chars": 186,
    "preview": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\n\nexport const Content = styled.div`"
  },
  {
    "path": "client/src/shared/components/TextEditedContent/index.jsx",
    "chars": 515,
    "preview": "/* eslint-disable react/no-danger */\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport 'quill/dist/q"
  },
  {
    "path": "client/src/shared/components/TextEditor/Styles.js",
    "chars": 513,
    "preview": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const EditorCont = s"
  },
  {
    "path": "client/src/shared/components/TextEditor/index.jsx",
    "chars": 2374,
    "preview": "import React, { useLayoutEffect, useRef } from 'react';\nimport PropTypes from 'prop-types';\nimport Quill from 'quill';\ni"
  },
  {
    "path": "client/src/shared/components/Textarea/Styles.js",
    "chars": 754,
    "preview": "import styled, { css } from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const Style"
  },
  {
    "path": "client/src/shared/components/Textarea/index.jsx",
    "chars": 892,
    "preview": "import React, { forwardRef } from 'react';\nimport PropTypes from 'prop-types';\nimport TextareaAutoSize from 'react-texta"
  },
  {
    "path": "client/src/shared/components/Tooltip/Styles.js",
    "chars": 339,
    "preview": "import styled from 'styled-components';\n\nimport { zIndexValues, mixin } from 'shared/utils/styles';\n\nexport const Styled"
  },
  {
    "path": "client/src/shared/components/Tooltip/index.jsx",
    "chars": 3217,
    "preview": "import React, { Fragment, useState, useRef, useLayoutEffect } from 'react';\nimport ReactDOM from 'react-dom';\nimport Pro"
  },
  {
    "path": "client/src/shared/components/index.js",
    "chars": 1264,
    "preview": "export { default as AboutTooltip } from './AboutTooltip';\nexport { default as Avatar } from './Avatar';\nexport { default"
  },
  {
    "path": "client/src/shared/constants/issues.js",
    "chars": 837,
    "preview": "export const IssueType = {\n  TASK: 'task',\n  BUG: 'bug',\n  STORY: 'story',\n};\n\nexport const IssueStatus = {\n  BACKLOG: '"
  },
  {
    "path": "client/src/shared/constants/keyCodes.js",
    "chars": 159,
    "preview": "export const KeyCodes = {\n  TAB: 9,\n  ENTER: 13,\n  ESCAPE: 27,\n  SPACE: 32,\n  ARROW_LEFT: 37,\n  ARROW_UP: 38,\n  ARROW_RI"
  },
  {
    "path": "client/src/shared/constants/projects.js",
    "chars": 279,
    "preview": "export const ProjectCategory = {\n  SOFTWARE: 'software',\n  MARKETING: 'marketing',\n  BUSINESS: 'business',\n};\n\nexport co"
  },
  {
    "path": "client/src/shared/hooks/api/index.js",
    "chars": 386,
    "preview": "import useQuery from './query';\nimport useMutation from './mutation';\n\n/* eslint-disable react-hooks/rules-of-hooks */\ne"
  },
  {
    "path": "client/src/shared/hooks/api/mutation.js",
    "chars": 1030,
    "preview": "import { useCallback } from 'react';\n\nimport api from 'shared/utils/api';\nimport useMergeState from 'shared/hooks/mergeS"
  },
  {
    "path": "client/src/shared/hooks/api/query.js",
    "chars": 2436,
    "preview": "import { useRef, useCallback, useEffect } from 'react';\nimport { isEqual } from 'lodash';\n\nimport api from 'shared/utils"
  },
  {
    "path": "client/src/shared/hooks/currentUser.js",
    "chars": 350,
    "preview": "import { get } from 'lodash';\n\nimport useApi from 'shared/hooks/api';\n\nconst useCurrentUser = ({ cachePolicy = 'cache-on"
  },
  {
    "path": "client/src/shared/hooks/deepCompareMemoize.js",
    "chars": 284,
    "preview": "import { useRef } from 'react';\nimport { isEqual } from 'lodash';\n\nconst useDeepCompareMemoize = value => {\n  const valu"
  },
  {
    "path": "client/src/shared/hooks/mergeState.js",
    "chars": 507,
    "preview": "import { useState, useCallback } from 'react';\nimport { isFunction } from 'lodash';\n\nconst useMergeState = initialState "
  },
  {
    "path": "client/src/shared/hooks/onEscapeKeyDown.js",
    "chars": 554,
    "preview": "import { useEffect } from 'react';\n\nimport { KeyCodes } from 'shared/constants/keyCodes';\n\nconst useOnEscapeKeyDown = (i"
  },
  {
    "path": "client/src/shared/hooks/onOutsideClick.js",
    "chars": 1418,
    "preview": "import { useEffect, useRef } from 'react';\n\nimport useDeepCompareMemoize from 'shared/hooks/deepCompareMemoize';\n\nconst "
  },
  {
    "path": "client/src/shared/utils/api.js",
    "chars": 1937,
    "preview": "import axios from 'axios';\n\nimport history from 'browserHistory';\nimport toast from 'shared/utils/toast';\nimport { objec"
  },
  {
    "path": "client/src/shared/utils/authToken.js",
    "chars": 239,
    "preview": "export const getStoredAuthToken = () => localStorage.getItem('authToken');\n\nexport const storeAuthToken = token => local"
  },
  {
    "path": "client/src/shared/utils/browser.js",
    "chars": 588,
    "preview": "export const getTextContentsFromHtmlString = html => {\n  const el = document.createElement('div');\n  el.innerHTML = html"
  },
  {
    "path": "client/src/shared/utils/dateTime.js",
    "chars": 464,
    "preview": "import moment from 'moment';\n\nexport const formatDate = (date, format = 'MMMM D, YYYY') =>\n  date ? moment(date).format("
  },
  {
    "path": "client/src/shared/utils/javascript.js",
    "chars": 779,
    "preview": "export const moveItemWithinArray = (arr, item, newIndex) => {\n  const arrClone = [...arr];\n  const oldIndex = arrClone.i"
  },
  {
    "path": "client/src/shared/utils/queryParamModal.js",
    "chars": 710,
    "preview": "import history from 'browserHistory';\nimport { queryStringToObject, addToQueryString, omitFromQueryString } from 'shared"
  },
  {
    "path": "client/src/shared/utils/styles.js",
    "chars": 4670,
    "preview": "import { css } from 'styled-components';\nimport Color from 'color';\n\nimport { IssueType, IssueStatus, IssuePriority } fr"
  },
  {
    "path": "client/src/shared/utils/toast.js",
    "chars": 340,
    "preview": "import pubsub from 'sweet-pubsub';\nimport { get } from 'lodash';\n\nconst show = toast => pubsub.emit('toast', toast);\n\nco"
  }
]

// ... and 9 more files (download for full content)

About this extraction

This page contains the full source code of the oldboyxx/jira_clone GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 209 files (276.0 KB), approximately 85.0k tokens, and a symbol index with 30 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!