[
  {
    "path": ".gitignore",
    "content": "# dependencies\n**/node_modules\n\n# misc\n**/.DS_Store\n\n# environment config\n**/.env\n\n# production\n**/build\n**/npm-debug.log*\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"eslint.workingDirectories\": [\n    {\n      \"directory\": \"./client\",\n      \"changeProcessCWD\": true\n    },\n    {\n      \"directory\": \"./api\",\n      \"changeProcessCWD\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "I will not be accepting PR's on this repository. Feel free to fork and maintain your own version.\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013-present, Yuxi (Evan) You\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">A simplified Jira clone built with React and Node</h1>\n\n<div align=\"center\">Auto formatted with Prettier, tested with Cypress 🎗</div>\n\n<h3 align=\"center\">\n  <a href=\"https://jira.ivorreic.com/\">Visit the live app</a> |\n  <a href=\"https://github.com/oldboyxx/jira_clone/tree/master/client\">View client</a> |\n  <a href=\"https://github.com/oldboyxx/jira_clone/tree/master/api\">View API</a>\n</h3>\n\n![Tech logos](https://i.ibb.co/DVFj8PL/tech-icons.jpg)\n\n![App screenshot](https://i.ibb.co/W3qVvCn/jira-optimized.jpg)\n\n## What is this and who is it for 🤷‍♀️\n\nI 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.\n\nThere 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.\n\n## Features\n\n- Proven, scalable, and easy to understand project structure\n- Written in modern React, only functional components with hooks\n- A variety of custom light-weight UI components such as datepicker, modal, various form elements etc\n- Simple local React state management, without redux, mobx, or similar\n- Custom webpack setup, without create-react-app or similar\n- Client written in Babel powered JavaScript\n- API written in TypeScript and using TypeORM\n\n## Setting up development environment 🛠\n\n- Install [postgreSQL](https://www.postgresql.org/) if you don't have it already and create a database named `jira_development`.\n- `git clone https://github.com/oldboyxx/jira_clone.git`\n- Create an empty `.env` file in `/api`, copy `/api/.env.example` contents into it, and fill in your database username and password.\n- `npm run install-dependencies`\n- `cd api && npm start`\n- `cd client && npm start` in another terminal tab\n- App should now be running on `http://localhost:8080/`\n\n## Running cypress end-to-end tests 🚥\n\n- Set up development environment\n- Create a database named `jira_test` and start the api with `cd api && npm run start:test`\n- `cd client && npm run test:cypress`\n\n## What's missing?\n\nThere are features missing from this showcase product which should exist in a real product:\n\n### Migrations 🗄\n\nWe'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).\n\n### Proper authentication system 🔐\n\nWe 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).\n\n### Accessibility ♿\n\nNot 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.\n\n### Unit/Integration tests 🧪\n\nBoth 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.\n\n## Contributing\n\nI will not be accepting PR's on this repository. Feel free to fork and maintain your own version.\n\n## License\n\n[MIT](https://opensource.org/licenses/MIT)\n\n<hr>\n\n<h3>\n  <a href=\"https://jira.ivorreic.com/\">Visit the live app</a> |\n  <a href=\"https://github.com/oldboyxx/jira_clone/tree/master/client\">View client</a> |\n  <a href=\"https://github.com/oldboyxx/jira_clone/tree/master/api\">View API</a>\n</h3>\n"
  },
  {
    "path": "api/.eslintignore",
    "content": "build/*\ntsconfig-paths.js\n"
  },
  {
    "path": "api/.eslintrc.json",
    "content": "{\n  \"parser\": \"@typescript-eslint/parser\",\n  \"parserOptions\": {\n    \"project\": \"./tsconfig.json\",\n    \"sourceType\": \"module\",\n    \"ecmaVersion\": 8\n  },\n  \"plugins\": [\"@typescript-eslint\"],\n  \"env\": {\n    \"node\": true\n  },\n  \"extends\": [\n    \"airbnb-base\",\n    \"plugin:import/typescript\",\n    \"plugin:@typescript-eslint/recommended\",\n    \"plugin:@typescript-eslint/recommended-requiring-type-checking\",\n    \"plugin:prettier/recommended\",\n    \"prettier/@typescript-eslint\"\n  ],\n  \"rules\": {\n    \"radix\": 0,\n    \"no-restricted-syntax\": 0,\n    \"no-await-in-loop\": 0,\n    \"no-console\": 0,\n    \"consistent-return\": 0,\n    \"@typescript-eslint/no-unused-vars\": 0,\n    \"@typescript-eslint/no-use-before-define\": 0,\n    \"@typescript-eslint/no-explicit-any\": 0,\n    \"import/prefer-default-export\": 0,\n    \"import/no-cycle\": 0\n  },\n  \"settings\": {\n    // Allows us to lint absolute imports within codebase\n    \"import/resolver\": {\n      \"node\": {\n        \"moduleDirectory\": [\"node_modules\", \"src/\"]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "api/.prettierrc",
    "content": "{\n  \"printWidth\": 100,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "api/README.md",
    "content": "# Project structure 🏗\n\nThe API codebase is fairly simple and should be easy enough to understand.\n\n<br>\n\n| File or folder    | Description                                                                                                                                                                                                                 |\n| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `src/index.ts`    | The entry file. This is where we setup middleware, attach routes, initialize database and express.                                                                                                                          |\n| `src/routes.ts`   | This is where we define all routes, both public and private.                                                                                                                                                                |\n| `src/constants`   | Constants are values that never change and are used in multiple places across the codebase.                                                                                                                                 |\n| `src/controllers` | Controllers listen to client's requests and work with entities and the database to fetch, add, update, or delete data.                                                                                                      |\n| `src/database`    | Database related code and seeds go here.                                                                                                                                                                                    |\n| `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.                                                                               |\n| `src/errors`      | This is where we define custom errors. The `catchErrors` function helps us avoid repetitive `try/catch` blocks within controllers.                                                                                          |\n| `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. |\n| `src/serializers` | Serializers transform the data fetched from the database before it's sent to the client.                                                                                                                                    |\n| `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.                                           |\n"
  },
  {
    "path": "api/package.json",
    "content": "{\n  \"name\": \"jira_api\",\n  \"version\": \"1.0.0\",\n  \"author\": \"Ivor Reic\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"nodemon --exec ts-node --files src/index.ts\",\n    \"start:test\": \"cross-env NODE_ENV='test' DB_DATABASE='jira_test' npm start\",\n    \"start:production\": \"pm2 start --name 'jira_api' node -- -r ./tsconfig-paths.js build/index.js\",\n    \"build\": \"cd src && tsc\",\n    \"pre-commit\": \"lint-staged\"\n  },\n  \"dependencies\": {\n    \"cors\": \"^2.8.5\",\n    \"dotenv\": \"^8.2.0\",\n    \"express\": \"^4.17.1\",\n    \"express-async-handler\": \"^1.1.4\",\n    \"faker\": \"^4.1.0\",\n    \"jsonwebtoken\": \"^8.5.1\",\n    \"lodash\": \"^4.17.15\",\n    \"module-alias\": \"^2.2.2\",\n    \"pg\": \"^7.14.0\",\n    \"reflect-metadata\": \"^0.1.13\",\n    \"striptags\": \"^3.1.1\",\n    \"typeorm\": \"^0.2.20\"\n  },\n  \"devDependencies\": {\n    \"@types/cors\": \"^2.8.6\",\n    \"@types/express\": \"^4.17.2\",\n    \"@types/faker\": \"^4.1.7\",\n    \"@types/jsonapi-serializer\": \"^3.6.2\",\n    \"@types/jsonwebtoken\": \"^8.3.5\",\n    \"@types/lodash\": \"^4.14.149\",\n    \"@types/node\": \"^12.12.11\",\n    \"@typescript-eslint/eslint-plugin\": \"^2.7.0\",\n    \"@typescript-eslint/parser\": \"^2.7.0\",\n    \"cross-env\": \"^6.0.3\",\n    \"eslint\": \"^6.1.0\",\n    \"eslint-config-airbnb-base\": \"^14.0.0\",\n    \"eslint-config-prettier\": \"^6.7.0\",\n    \"eslint-plugin-import\": \"^2.18.2\",\n    \"eslint-plugin-prettier\": \"^3.1.1\",\n    \"lint-staged\": \"^9.4.3\",\n    \"nodemon\": \"^2.0.0\",\n    \"prettier\": \"^1.19.1\",\n    \"ts-node\": \"^8.5.2\",\n    \"tsconfig-paths\": \"^3.9.0\",\n    \"typescript\": \"^3.7.2\"\n  },\n  \"_moduleDirectories\": [\n    \"src\"\n  ],\n  \"lint-staged\": {\n    \"*.ts\": [\n      \"eslint --fix\",\n      \"prettier --write\",\n      \"git add\"\n    ]\n  }\n}\n"
  },
  {
    "path": "api/src/constants/issues.ts",
    "content": "export enum IssueType {\n  TASK = 'task',\n  BUG = 'bug',\n  STORY = 'story',\n}\n\nexport enum IssueStatus {\n  BACKLOG = 'backlog',\n  SELECTED = 'selected',\n  INPROGRESS = 'inprogress',\n  DONE = 'done',\n}\n\nexport enum IssuePriority {\n  HIGHEST = '5',\n  HIGH = '4',\n  MEDIUM = '3',\n  LOW = '2',\n  LOWEST = '1',\n}\n"
  },
  {
    "path": "api/src/constants/projects.ts",
    "content": "export enum ProjectCategory {\n  SOFTWARE = 'software',\n  MARKETING = 'marketing',\n  BUSINESS = 'business',\n}\n"
  },
  {
    "path": "api/src/controllers/authentication.ts",
    "content": "import { catchErrors } from 'errors';\nimport { signToken } from 'utils/authToken';\nimport createAccount from 'database/createGuestAccount';\n\nexport const createGuestAccount = catchErrors(async (_req, res) => {\n  const user = await createAccount();\n  res.respond({\n    authToken: signToken({ sub: user.id }),\n  });\n});\n"
  },
  {
    "path": "api/src/controllers/comments.ts",
    "content": "import { Comment } from 'entities';\nimport { catchErrors } from 'errors';\nimport { updateEntity, deleteEntity, createEntity } from 'utils/typeorm';\n\nexport const create = catchErrors(async (req, res) => {\n  const comment = await createEntity(Comment, req.body);\n  res.respond({ comment });\n});\n\nexport const update = catchErrors(async (req, res) => {\n  const comment = await updateEntity(Comment, req.params.commentId, req.body);\n  res.respond({ comment });\n});\n\nexport const remove = catchErrors(async (req, res) => {\n  const comment = await deleteEntity(Comment, req.params.commentId);\n  res.respond({ comment });\n});\n"
  },
  {
    "path": "api/src/controllers/issues.ts",
    "content": "import { Issue } from 'entities';\nimport { catchErrors } from 'errors';\nimport { updateEntity, deleteEntity, createEntity, findEntityOrThrow } from 'utils/typeorm';\n\nexport const getProjectIssues = catchErrors(async (req, res) => {\n  const { projectId } = req.currentUser;\n  const { searchTerm } = req.query;\n\n  let whereSQL = 'issue.projectId = :projectId';\n\n  if (searchTerm) {\n    whereSQL += ' AND (issue.title ILIKE :searchTerm OR issue.descriptionText ILIKE :searchTerm)';\n  }\n\n  const issues = await Issue.createQueryBuilder('issue')\n    .select()\n    .where(whereSQL, { projectId, searchTerm: `%${searchTerm}%` })\n    .getMany();\n\n  res.respond({ issues });\n});\n\nexport const getIssueWithUsersAndComments = catchErrors(async (req, res) => {\n  const issue = await findEntityOrThrow(Issue, req.params.issueId, {\n    relations: ['users', 'comments', 'comments.user'],\n  });\n  res.respond({ issue });\n});\n\nexport const create = catchErrors(async (req, res) => {\n  const listPosition = await calculateListPosition(req.body);\n  const issue = await createEntity(Issue, { ...req.body, listPosition });\n  res.respond({ issue });\n});\n\nexport const update = catchErrors(async (req, res) => {\n  const issue = await updateEntity(Issue, req.params.issueId, req.body);\n  res.respond({ issue });\n});\n\nexport const remove = catchErrors(async (req, res) => {\n  const issue = await deleteEntity(Issue, req.params.issueId);\n  res.respond({ issue });\n});\n\nconst calculateListPosition = async ({ projectId, status }: Issue): Promise<number> => {\n  const issues = await Issue.find({ projectId, status });\n\n  const listPositions = issues.map(({ listPosition }) => listPosition);\n\n  if (listPositions.length > 0) {\n    return Math.min(...listPositions) - 1;\n  }\n  return 1;\n};\n"
  },
  {
    "path": "api/src/controllers/projects.ts",
    "content": "import { Project } from 'entities';\nimport { catchErrors } from 'errors';\nimport { findEntityOrThrow, updateEntity } from 'utils/typeorm';\nimport { issuePartial } from 'serializers/issues';\n\nexport const getProjectWithUsersAndIssues = catchErrors(async (req, res) => {\n  const project = await findEntityOrThrow(Project, req.currentUser.projectId, {\n    relations: ['users', 'issues'],\n  });\n  res.respond({\n    project: {\n      ...project,\n      issues: project.issues.map(issuePartial),\n    },\n  });\n});\n\nexport const update = catchErrors(async (req, res) => {\n  const project = await updateEntity(Project, req.currentUser.projectId, req.body);\n  res.respond({ project });\n});\n"
  },
  {
    "path": "api/src/controllers/test.ts",
    "content": "import { catchErrors } from 'errors';\nimport { signToken } from 'utils/authToken';\nimport resetTestDatabase from 'database/resetDatabase';\nimport createTestAccount from 'database/createTestAccount';\n\nexport const resetDatabase = catchErrors(async (_req, res) => {\n  await resetTestDatabase();\n  res.respond(true);\n});\n\nexport const createAccount = catchErrors(async (_req, res) => {\n  const user = await createTestAccount();\n  res.respond({\n    authToken: signToken({ sub: user.id }),\n  });\n});\n"
  },
  {
    "path": "api/src/controllers/users.ts",
    "content": "import { catchErrors } from 'errors';\n\nexport const getCurrentUser = catchErrors((req, res) => {\n  res.respond({ currentUser: req.currentUser });\n});\n"
  },
  {
    "path": "api/src/database/createConnection.ts",
    "content": "import { createConnection, Connection } from 'typeorm';\n\nimport * as entities from 'entities';\n\nconst createDatabaseConnection = (): Promise<Connection> =>\n  createConnection({\n    type: 'postgres',\n    host: process.env.DB_HOST,\n    port: Number(process.env.DB_PORT),\n    username: process.env.DB_USERNAME,\n    password: process.env.DB_PASSWORD,\n    database: process.env.DB_DATABASE,\n    entities: Object.values(entities),\n    synchronize: true,\n  });\n\nexport default createDatabaseConnection;\n"
  },
  {
    "path": "api/src/database/createGuestAccount.ts",
    "content": "import { Comment, Issue, Project, User } from 'entities';\nimport { ProjectCategory } from 'constants/projects';\nimport { IssueType, IssueStatus, IssuePriority } from 'constants/issues';\nimport { createEntity } from 'utils/typeorm';\n\nconst seedUsers = (): Promise<User[]> => {\n  const users = [\n    createEntity(User, {\n      email: 'rick@jira.guest',\n      name: 'Pickle Rick',\n      avatarUrl: 'https://i.ibb.co/7JM1P2r/picke-rick.jpg',\n    }),\n    createEntity(User, {\n      email: 'yoda@jira.guest',\n      name: 'Baby Yoda',\n      avatarUrl: 'https://i.ibb.co/6n0hLML/baby-yoda.jpg',\n    }),\n    createEntity(User, {\n      email: 'gaben@jira.guest',\n      name: 'Lord Gaben',\n      avatarUrl: 'https://i.ibb.co/6RJ5hq6/gaben.jpg',\n    }),\n  ];\n  return Promise.all(users);\n};\n\nconst seedProject = (users: User[]): Promise<Project> =>\n  createEntity(Project, {\n    name: 'singularity 1.0',\n    url: 'https://www.atlassian.com/software/jira',\n    description:\n      'Plan, track, and manage your agile and software development projects in Jira. Customize your workflow, collaborate, and release great software.',\n    category: ProjectCategory.SOFTWARE,\n    users,\n  });\n\nconst seedIssues = (project: Project): Promise<Issue[]> => {\n  const { users } = project;\n\n  const issues = [\n    createEntity(Issue, {\n      title: 'This is an issue of type: Task.',\n      type: IssueType.TASK,\n      status: IssueStatus.BACKLOG,\n      priority: IssuePriority.HIGH,\n      listPosition: 1,\n      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>`,\n      estimate: 8,\n      timeSpent: 4,\n      reporterId: users[1].id,\n      project,\n      users: [users[0]],\n    }),\n    createEntity(Issue, {\n      title: \"Click on an issue to see what's behind it.\",\n      type: IssueType.TASK,\n      status: IssueStatus.BACKLOG,\n      priority: IssuePriority.LOW,\n      listPosition: 2,\n      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>`,\n      estimate: 5,\n      timeSpent: 2,\n      reporterId: users[2].id,\n      project,\n      users: [users[0]],\n    }),\n    createEntity(Issue, {\n      title: 'Try dragging issues to different columns to transition their status.',\n      type: IssueType.STORY,\n      status: IssueStatus.BACKLOG,\n      priority: IssuePriority.MEDIUM,\n      listPosition: 3,\n      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>`,\n      estimate: 15,\n      timeSpent: 12,\n      reporterId: users[1].id,\n      project,\n    }),\n    createEntity(Issue, {\n      title: 'You can use rich text with images in issue descriptions.',\n      type: IssueType.STORY,\n      status: IssueStatus.BACKLOG,\n      priority: IssuePriority.LOWEST,\n      listPosition: 4,\n      description: `<h1><span style=\"color: rgb(51, 51, 51);\">🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🍈 🍒 🍑 🍍 🥭 🥥 🥝 🍅 🍆 🥑 🥦 🥒 🥬 🌶 🌽 🥕 🥔 🍠 🥐 🍞 🥖 🥨 🥯 🧀 🥚 🍳 🥞 🥓 🥩 🍗 🍖 🌭 🍔 🍟 🍕 🥪 🥙 🌮 🌯 🥗 🥘 🥫 🍝 🍜 🍲 🍛 🍣 🍱 🥟 🍤 🍙 🍚 🍘 🍥 🥮 🥠 🍢 🍡 🍧 🍨 🍦 🥧 🍰 🎂 🍮 🍭 🍬 🍫 🍿 🧂 🍩 🍪 🌰 🥜 🍯 🥛 🍼 ☕️ 🍵 🥤 🍶 🍺 🍻 🥂 🍷 🥃 🍸 🍹 🍾 🥄 🍴 🍽 🥣 🥡 🥢</span></h1>`,\n      estimate: 4,\n      timeSpent: 4,\n      reporterId: users[0].id,\n      project,\n      users: [users[2]],\n    }),\n    createEntity(Issue, {\n      title: 'Each issue can be assigned priority from lowest to highest.',\n      type: IssueType.TASK,\n      status: IssueStatus.SELECTED,\n      priority: IssuePriority.HIGHEST,\n      listPosition: 5,\n      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>`,\n      estimate: 4,\n      timeSpent: 1,\n      reporterId: users[2].id,\n      project,\n    }),\n    createEntity(Issue, {\n      title: 'Each issue has a single reporter but can have multiple assignees.',\n      type: IssueType.STORY,\n      status: IssueStatus.SELECTED,\n      priority: IssuePriority.HIGH,\n      listPosition: 6,\n      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>`,\n      estimate: 6,\n      timeSpent: 3,\n      reporterId: users[1].id,\n      project,\n      users: [users[1], users[2]],\n    }),\n    createEntity(Issue, {\n      title:\n        'You can track how many hours were spent working on an issue, and how many hours remain.',\n      type: IssueType.TASK,\n      status: IssueStatus.INPROGRESS,\n      priority: IssuePriority.LOWEST,\n      listPosition: 7,\n      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>`,\n      estimate: 12,\n      timeSpent: 11,\n      reporterId: users[0].id,\n      project,\n    }),\n    createEntity(Issue, {\n      title: 'Try leaving a comment on this issue.',\n      type: IssueType.TASK,\n      status: IssueStatus.DONE,\n      priority: IssuePriority.MEDIUM,\n      listPosition: 7,\n      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>`,\n      estimate: 10,\n      timeSpent: 2,\n      reporterId: users[0].id,\n      project,\n      users: [users[1]],\n    }),\n  ];\n  return Promise.all(issues);\n};\n\nconst seedComments = (issues: Issue[], users: User[]): Promise<Comment[]> => {\n  const comments = [\n    createEntity(Comment, {\n      body: 'An old silent pond...\\nA frog jumps into the pond,\\nsplash! Silence again.',\n      issueId: issues[0].id,\n      userId: users[2].id,\n    }),\n    createEntity(Comment, {\n      body: 'Autumn moonlight-\\na worm digs silently\\ninto the chestnut.',\n      issueId: issues[1].id,\n      userId: users[2].id,\n    }),\n    createEntity(Comment, {\n      body: 'In the twilight rain\\nthese brilliant-hued hibiscus -\\nA lovely sunset.',\n      issueId: issues[2].id,\n      userId: users[2].id,\n    }),\n    createEntity(Comment, {\n      body: 'A summer river being crossed\\nhow pleasing\\nwith sandals in my hands!',\n      issueId: issues[3].id,\n      userId: users[2].id,\n    }),\n    createEntity(Comment, {\n      body: \"Light of the moon\\nMoves west, flowers' shadows\\nCreep eastward.\",\n      issueId: issues[4].id,\n      userId: users[2].id,\n    }),\n    createEntity(Comment, {\n      body: 'In the moonlight,\\nThe color and scent of the wisteria\\nSeems far away.',\n      issueId: issues[5].id,\n      userId: users[2].id,\n    }),\n    createEntity(Comment, {\n      body: 'O snail\\nClimb Mount Fuji,\\nBut slowly, slowly!',\n      issueId: issues[6].id,\n      userId: users[2].id,\n    }),\n    createEntity(Comment, {\n      body: 'Everything I touch\\nwith tenderness, alas,\\npricks like a bramble.',\n      issueId: issues[7].id,\n      userId: users[2].id,\n    }),\n  ];\n  return Promise.all(comments);\n};\n\nconst createGuestAccount = async (): Promise<User> => {\n  const users = await seedUsers();\n  const project = await seedProject(users);\n  const issues = await seedIssues(project);\n  await seedComments(issues, project.users);\n  return users[2];\n};\n\nexport default createGuestAccount;\n"
  },
  {
    "path": "api/src/database/createTestAccount.ts",
    "content": "import { Comment, Issue, Project, User } from 'entities';\nimport { ProjectCategory } from 'constants/projects';\nimport { IssueType, IssueStatus, IssuePriority } from 'constants/issues';\nimport { createEntity } from 'utils/typeorm';\n\nconst seedUsers = (): Promise<User[]> => {\n  const users = [\n    createEntity(User, {\n      email: 'gaben@jira.test',\n      name: 'Gaben',\n      avatarUrl: 'https://i.ibb.co/6RJ5hq6/gaben.jpg',\n    }),\n    createEntity(User, {\n      email: 'yoda@jira.test',\n      name: 'Yoda',\n      avatarUrl: 'https://i.ibb.co/6n0hLML/baby-yoda.jpg',\n    }),\n  ];\n  return Promise.all(users);\n};\n\nconst seedProject = (users: User[]): Promise<Project> =>\n  createEntity(Project, {\n    name: 'Project name',\n    url: 'https://www.testurl.com',\n    description: 'Project description',\n    category: ProjectCategory.SOFTWARE,\n    users,\n  });\n\nconst seedIssues = (project: Project): Promise<Issue[]> => {\n  const { users } = project;\n\n  const issues = [\n    createEntity(Issue, {\n      title: 'Issue title 1',\n      type: IssueType.TASK,\n      status: IssueStatus.BACKLOG,\n      priority: IssuePriority.LOWEST,\n      listPosition: 1,\n      reporterId: users[0].id,\n      project,\n    }),\n    createEntity(Issue, {\n      title: 'Issue title 2',\n      type: IssueType.TASK,\n      status: IssueStatus.BACKLOG,\n      priority: IssuePriority.MEDIUM,\n      listPosition: 2,\n      estimate: 5,\n      description: 'Issue description 2',\n      reporterId: users[0].id,\n      users: [users[0]],\n      project,\n    }),\n    createEntity(Issue, {\n      title: 'Issue title 3',\n      type: IssueType.STORY,\n      status: IssueStatus.SELECTED,\n      priority: IssuePriority.HIGH,\n      listPosition: 3,\n      estimate: 10,\n      description: 'Issue description 3',\n      reporterId: users[0].id,\n      users: [users[0], users[1]],\n      project,\n    }),\n  ];\n  return Promise.all(issues);\n};\n\nconst seedComments = (issue: Issue, user: User): Promise<Comment> =>\n  createEntity(Comment, {\n    body: 'Comment body',\n    issueId: issue.id,\n    userId: user.id,\n  });\n\nconst createTestAccount = async (): Promise<User> => {\n  const users = await seedUsers();\n  const project = await seedProject(users);\n  const issues = await seedIssues(project);\n  await seedComments(issues[0], project.users[0]);\n  return users[0];\n};\n\nexport default createTestAccount;\n"
  },
  {
    "path": "api/src/database/resetDatabase.ts",
    "content": "import { getConnection } from 'typeorm';\n\nconst resetDatabase = async (): Promise<void> => {\n  const connection = getConnection();\n  await connection.dropDatabase();\n  await connection.synchronize();\n};\n\nexport default resetDatabase;\n"
  },
  {
    "path": "api/src/entities/Comment.ts",
    "content": "import {\n  BaseEntity,\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  UpdateDateColumn,\n  ManyToOne,\n} from 'typeorm';\n\nimport is from 'utils/validation';\nimport { Issue, User } from '.';\n\n@Entity()\nclass Comment extends BaseEntity {\n  static validations = {\n    body: [is.required(), is.maxLength(50000)],\n  };\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column('text')\n  body: string;\n\n  @CreateDateColumn({ type: 'timestamp' })\n  createdAt: Date;\n\n  @UpdateDateColumn({ type: 'timestamp' })\n  updatedAt: Date;\n\n  @ManyToOne(\n    () => User,\n    user => user.comments,\n  )\n  user: User;\n\n  @Column('integer')\n  userId: number;\n\n  @ManyToOne(\n    () => Issue,\n    issue => issue.comments,\n    { onDelete: 'CASCADE' },\n  )\n  issue: Issue;\n\n  @Column('integer')\n  issueId: number;\n}\n\nexport default Comment;\n"
  },
  {
    "path": "api/src/entities/Issue.ts",
    "content": "import striptags from 'striptags';\nimport {\n  BaseEntity,\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  UpdateDateColumn,\n  ManyToOne,\n  OneToMany,\n  ManyToMany,\n  JoinTable,\n  RelationId,\n  BeforeUpdate,\n  BeforeInsert,\n} from 'typeorm';\n\nimport is from 'utils/validation';\nimport { IssueType, IssueStatus, IssuePriority } from 'constants/issues';\nimport { Comment, Project, User } from '.';\n\n@Entity()\nclass Issue extends BaseEntity {\n  static validations = {\n    title: [is.required(), is.maxLength(200)],\n    type: [is.required(), is.oneOf(Object.values(IssueType))],\n    status: [is.required(), is.oneOf(Object.values(IssueStatus))],\n    priority: [is.required(), is.oneOf(Object.values(IssuePriority))],\n    listPosition: is.required(),\n    reporterId: is.required(),\n  };\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column('varchar')\n  title: string;\n\n  @Column('varchar')\n  type: IssueType;\n\n  @Column('varchar')\n  status: IssueStatus;\n\n  @Column('varchar')\n  priority: IssuePriority;\n\n  @Column('double precision')\n  listPosition: number;\n\n  @Column('text', { nullable: true })\n  description: string | null;\n\n  @Column('text', { nullable: true })\n  descriptionText: string | null;\n\n  @Column('integer', { nullable: true })\n  estimate: number | null;\n\n  @Column('integer', { nullable: true })\n  timeSpent: number | null;\n\n  @Column('integer', { nullable: true })\n  timeRemaining: number | null;\n\n  @CreateDateColumn({ type: 'timestamp' })\n  createdAt: Date;\n\n  @UpdateDateColumn({ type: 'timestamp' })\n  updatedAt: Date;\n\n  @Column('integer')\n  reporterId: number;\n\n  @ManyToOne(\n    () => Project,\n    project => project.issues,\n  )\n  project: Project;\n\n  @Column('integer')\n  projectId: number;\n\n  @OneToMany(\n    () => Comment,\n    comment => comment.issue,\n  )\n  comments: Comment[];\n\n  @ManyToMany(\n    () => User,\n    user => user.issues,\n  )\n  @JoinTable()\n  users: User[];\n\n  @RelationId((issue: Issue) => issue.users)\n  userIds: number[];\n\n  @BeforeInsert()\n  @BeforeUpdate()\n  setDescriptionText = (): void => {\n    if (this.description) {\n      this.descriptionText = striptags(this.description);\n    }\n  };\n}\n\nexport default Issue;\n"
  },
  {
    "path": "api/src/entities/Project.ts",
    "content": "import {\n  BaseEntity,\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  UpdateDateColumn,\n  OneToMany,\n} from 'typeorm';\n\nimport is from 'utils/validation';\nimport { ProjectCategory } from 'constants/projects';\nimport { Issue, User } from '.';\n\n@Entity()\nclass Project extends BaseEntity {\n  static validations = {\n    name: [is.required(), is.maxLength(100)],\n    url: is.url(),\n    category: [is.required(), is.oneOf(Object.values(ProjectCategory))],\n  };\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column('varchar')\n  name: string;\n\n  @Column('varchar', { nullable: true })\n  url: string | null;\n\n  @Column('text', { nullable: true })\n  description: string | null;\n\n  @Column('varchar')\n  category: ProjectCategory;\n\n  @CreateDateColumn({ type: 'timestamp' })\n  createdAt: Date;\n\n  @UpdateDateColumn({ type: 'timestamp' })\n  updatedAt: Date;\n\n  @OneToMany(\n    () => Issue,\n    issue => issue.project,\n  )\n  issues: Issue[];\n\n  @OneToMany(\n    () => User,\n    user => user.project,\n  )\n  users: User[];\n}\n\nexport default Project;\n"
  },
  {
    "path": "api/src/entities/User.ts",
    "content": "import {\n  BaseEntity,\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  UpdateDateColumn,\n  OneToMany,\n  ManyToMany,\n  ManyToOne,\n  RelationId,\n} from 'typeorm';\n\nimport is from 'utils/validation';\nimport { Comment, Issue, Project } from '.';\n\n@Entity()\nclass User extends BaseEntity {\n  static validations = {\n    name: [is.required(), is.maxLength(100)],\n    email: [is.required(), is.email(), is.maxLength(200)],\n  };\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column('varchar')\n  name: string;\n\n  @Column('varchar')\n  email: string;\n\n  @Column('varchar', { length: 2000 })\n  avatarUrl: string;\n\n  @CreateDateColumn({ type: 'timestamp' })\n  createdAt: Date;\n\n  @UpdateDateColumn({ type: 'timestamp' })\n  updatedAt: Date;\n\n  @OneToMany(\n    () => Comment,\n    comment => comment.user,\n  )\n  comments: Comment[];\n\n  @ManyToMany(\n    () => Issue,\n    issue => issue.users,\n  )\n  issues: Issue[];\n\n  @ManyToOne(\n    () => Project,\n    project => project.users,\n  )\n  project: Project;\n\n  @RelationId((user: User) => user.project)\n  projectId: number;\n}\n\nexport default User;\n"
  },
  {
    "path": "api/src/entities/index.ts",
    "content": "export { default as Comment } from './Comment';\nexport { default as Issue } from './Issue';\nexport { default as Project } from './Project';\nexport { default as User } from './User';\n"
  },
  {
    "path": "api/src/errors/asyncCatch.ts",
    "content": "import { RequestHandler } from 'express';\n\nexport const catchErrors = (requestHandler: RequestHandler): RequestHandler => {\n  return async (req, res, next): Promise<any> => {\n    try {\n      return await requestHandler(req, res, next);\n    } catch (error) {\n      next(error);\n    }\n  };\n};\n"
  },
  {
    "path": "api/src/errors/customErrors.ts",
    "content": "/* eslint-disable max-classes-per-file */\n\ntype ErrorData = { [key: string]: any };\n\nexport class CustomError extends Error {\n  constructor(\n    public message: string,\n    public code: string | number = 'INTERNAL_ERROR',\n    public status: number = 500,\n    public data: ErrorData = {},\n  ) {\n    super();\n  }\n}\n\nexport class RouteNotFoundError extends CustomError {\n  constructor(originalUrl: string) {\n    super(`Route '${originalUrl}' does not exist.`, 'ROUTE_NOT_FOUND', 404);\n  }\n}\n\nexport class EntityNotFoundError extends CustomError {\n  constructor(entityName: string) {\n    super(`${entityName} not found.`, 'ENTITY_NOT_FOUND', 404);\n  }\n}\n\nexport class BadUserInputError extends CustomError {\n  constructor(errorData: ErrorData) {\n    super('There were validation errors.', 'BAD_USER_INPUT', 400, errorData);\n  }\n}\n\nexport class InvalidTokenError extends CustomError {\n  constructor(message = 'Authentication token is invalid.') {\n    super(message, 'INVALID_TOKEN', 401);\n  }\n}\n"
  },
  {
    "path": "api/src/errors/index.ts",
    "content": "export * from './customErrors';\nexport { catchErrors } from './asyncCatch';\n"
  },
  {
    "path": "api/src/index.ts",
    "content": "import 'module-alias/register';\nimport 'dotenv/config';\nimport 'reflect-metadata';\nimport express from 'express';\nimport cors from 'cors';\n\nimport createDatabaseConnection from 'database/createConnection';\nimport { addRespondToResponse } from 'middleware/response';\nimport { authenticateUser } from 'middleware/authentication';\nimport { handleError } from 'middleware/errors';\nimport { RouteNotFoundError } from 'errors';\n\nimport { attachPublicRoutes, attachPrivateRoutes } from './routes';\n\nconst establishDatabaseConnection = async (): Promise<void> => {\n  try {\n    await createDatabaseConnection();\n  } catch (error) {\n    console.log(error);\n  }\n};\n\nconst initializeExpress = (): void => {\n  const app = express();\n\n  app.use(cors());\n  app.use(express.json());\n  app.use(express.urlencoded());\n\n  app.use(addRespondToResponse);\n\n  attachPublicRoutes(app);\n\n  app.use('/', authenticateUser);\n\n  attachPrivateRoutes(app);\n\n  app.use((req, _res, next) => next(new RouteNotFoundError(req.originalUrl)));\n  app.use(handleError);\n\n  app.listen(process.env.PORT || 3000);\n};\n\nconst initializeApp = async (): Promise<void> => {\n  await establishDatabaseConnection();\n  initializeExpress();\n};\n\ninitializeApp();\n"
  },
  {
    "path": "api/src/middleware/authentication.ts",
    "content": "import { Request } from 'express';\n\nimport { verifyToken } from 'utils/authToken';\nimport { catchErrors, InvalidTokenError } from 'errors';\nimport { User } from 'entities';\n\nexport const authenticateUser = catchErrors(async (req, _res, next) => {\n  const token = getAuthTokenFromRequest(req);\n  if (!token) {\n    throw new InvalidTokenError('Authentication token not found.');\n  }\n  const userId = verifyToken(token).sub;\n  if (!userId) {\n    throw new InvalidTokenError('Authentication token is invalid.');\n  }\n  const user = await User.findOne(userId);\n  if (!user) {\n    throw new InvalidTokenError('Authentication token is invalid: User not found.');\n  }\n  req.currentUser = user;\n  next();\n});\n\nconst getAuthTokenFromRequest = (req: Request): string | null => {\n  const header = req.get('Authorization') || '';\n  const [bearer, token] = header.split(' ');\n  return bearer === 'Bearer' && token ? token : null;\n};\n"
  },
  {
    "path": "api/src/middleware/errors.ts",
    "content": "import { ErrorRequestHandler } from 'express';\nimport { pick } from 'lodash';\n\nimport { CustomError } from 'errors';\n\nexport const handleError: ErrorRequestHandler = (error, _req, res, _next) => {\n  console.error(error);\n\n  const isErrorSafeForClient = error instanceof CustomError;\n\n  const clientError = isErrorSafeForClient\n    ? pick(error, ['message', 'code', 'status', 'data'])\n    : {\n        message: 'Something went wrong, please contact our support.',\n        code: 'INTERNAL_ERROR',\n        status: 500,\n        data: {},\n      };\n\n  res.status(clientError.status).send({ error: clientError });\n};\n"
  },
  {
    "path": "api/src/middleware/response.ts",
    "content": "import { RequestHandler } from 'express';\n\nexport const addRespondToResponse: RequestHandler = (_req, res, next) => {\n  res.respond = (data): void => {\n    res.status(200).send(data);\n  };\n  next();\n};\n"
  },
  {
    "path": "api/src/routes.ts",
    "content": "import * as authentication from 'controllers/authentication';\nimport * as comments from 'controllers/comments';\nimport * as issues from 'controllers/issues';\nimport * as projects from 'controllers/projects';\nimport * as test from 'controllers/test';\nimport * as users from 'controllers/users';\n\nexport const attachPublicRoutes = (app: any): void => {\n  if (process.env.NODE_ENV === 'test') {\n    app.delete('/test/reset-database', test.resetDatabase);\n    app.post('/test/create-account', test.createAccount);\n  }\n\n  app.post('/authentication/guest', authentication.createGuestAccount);\n};\n\nexport const attachPrivateRoutes = (app: any): void => {\n  app.post('/comments', comments.create);\n  app.put('/comments/:commentId', comments.update);\n  app.delete('/comments/:commentId', comments.remove);\n\n  app.get('/issues', issues.getProjectIssues);\n  app.get('/issues/:issueId', issues.getIssueWithUsersAndComments);\n  app.post('/issues', issues.create);\n  app.put('/issues/:issueId', issues.update);\n  app.delete('/issues/:issueId', issues.remove);\n\n  app.get('/project', projects.getProjectWithUsersAndIssues);\n  app.put('/project', projects.update);\n\n  app.get('/currentUser', users.getCurrentUser);\n};\n"
  },
  {
    "path": "api/src/serializers/issues.ts",
    "content": "import { pick } from 'lodash';\n\nimport { Issue } from 'entities';\n\nexport const issuePartial = (issue: Issue): Partial<Issue> =>\n  pick(issue, [\n    'id',\n    'title',\n    'type',\n    'status',\n    'priority',\n    'listPosition',\n    'createdAt',\n    'updatedAt',\n    'userIds',\n  ]);\n"
  },
  {
    "path": "api/src/types/env.d.ts",
    "content": "declare namespace NodeJS {\n  export interface ProcessEnv {\n    DB_HOST: string;\n    DB_PORT: string;\n    DB_USERNAME: string;\n    DB_PASSWORD: string;\n    DB_DATABASE: string;\n    JWT_SECRET: string;\n  }\n}\n"
  },
  {
    "path": "api/src/types/express.d.ts",
    "content": "declare namespace Express {\n  export interface Response {\n    respond: (data: any) => void;\n  }\n  export interface Request {\n    currentUser: import('entities').User;\n  }\n}\n"
  },
  {
    "path": "api/src/utils/authToken.ts",
    "content": "import jwt, { SignOptions } from 'jsonwebtoken';\nimport { isPlainObject } from 'lodash';\n\nimport { InvalidTokenError } from 'errors';\n\nexport const signToken = (payload: object, options?: SignOptions): string =>\n  jwt.sign(payload, process.env.JWT_SECRET, {\n    expiresIn: '180 days',\n    ...options,\n  });\n\nexport const verifyToken = (token: string): { [key: string]: any } => {\n  try {\n    const payload = jwt.verify(token, process.env.JWT_SECRET);\n\n    if (isPlainObject(payload)) {\n      return payload as { [key: string]: any };\n    }\n    throw new Error();\n  } catch (error) {\n    throw new InvalidTokenError();\n  }\n};\n"
  },
  {
    "path": "api/src/utils/typeorm.ts",
    "content": "import { FindOneOptions } from 'typeorm/find-options/FindOneOptions';\n\nimport { Project, User, Issue, Comment } from 'entities';\nimport { EntityNotFoundError, BadUserInputError } from 'errors';\nimport { generateErrors } from 'utils/validation';\n\ntype EntityConstructor = typeof Project | typeof User | typeof Issue | typeof Comment;\ntype EntityInstance = Project | User | Issue | Comment;\n\nconst entities: { [key: string]: EntityConstructor } = { Comment, Issue, Project, User };\n\nexport const findEntityOrThrow = async <T extends EntityConstructor>(\n  Constructor: T,\n  id: number | string,\n  options?: FindOneOptions,\n): Promise<InstanceType<T>> => {\n  const instance = await Constructor.findOne(id, options);\n  if (!instance) {\n    throw new EntityNotFoundError(Constructor.name);\n  }\n  return instance;\n};\n\nexport const validateAndSaveEntity = async <T extends EntityInstance>(instance: T): Promise<T> => {\n  const Constructor = entities[instance.constructor.name];\n\n  if ('validations' in Constructor) {\n    const errorFields = generateErrors(instance, Constructor.validations);\n\n    if (Object.keys(errorFields).length > 0) {\n      throw new BadUserInputError({ fields: errorFields });\n    }\n  }\n  return instance.save() as Promise<T>;\n};\n\nexport const createEntity = async <T extends EntityConstructor>(\n  Constructor: T,\n  input: Partial<InstanceType<T>>,\n): Promise<InstanceType<T>> => {\n  const instance = Constructor.create(input);\n  return validateAndSaveEntity(instance as InstanceType<T>);\n};\n\nexport const updateEntity = async <T extends EntityConstructor>(\n  Constructor: T,\n  id: number | string,\n  input: Partial<InstanceType<T>>,\n): Promise<InstanceType<T>> => {\n  const instance = await findEntityOrThrow(Constructor, id);\n  Object.assign(instance, input);\n  return validateAndSaveEntity(instance);\n};\n\nexport const deleteEntity = async <T extends EntityConstructor>(\n  Constructor: T,\n  id: number | string,\n): Promise<InstanceType<T>> => {\n  const instance = await findEntityOrThrow(Constructor, id);\n  await instance.remove();\n  return instance;\n};\n"
  },
  {
    "path": "api/src/utils/validation.ts",
    "content": "type Value = any;\ntype ErrorMessage = false | string;\ntype FieldValues = { [key: string]: Value };\ntype Validator = (value: Value, fieldValues?: FieldValues) => ErrorMessage;\ntype FieldValidators = { [key: string]: Validator | Validator[] };\ntype FieldErrors = { [key: string]: string };\n\nconst is = {\n  match: (testFn: Function, message = '') => (\n    value: Value,\n    fieldValues: FieldValues,\n  ): ErrorMessage => !testFn(value, fieldValues) && message,\n\n  required: () => (value: Value): ErrorMessage =>\n    isNilOrEmptyString(value) && 'This field is required',\n\n  minLength: (min: number) => (value: Value): ErrorMessage =>\n    !!value && value.length < min && `Must be at least ${min} characters`,\n\n  maxLength: (max: number) => (value: Value): ErrorMessage =>\n    !!value && value.length > max && `Must be at most ${max} characters`,\n\n  oneOf: (arr: any[]) => (value: Value): ErrorMessage =>\n    !!value && !arr.includes(value) && `Must be one of: ${arr.join(', ')}`,\n\n  notEmptyArray: () => (value: Value): ErrorMessage =>\n    Array.isArray(value) && value.length === 0 && 'Please add at least one item',\n\n  email: () => (value: Value): ErrorMessage =>\n    !!value && !/.+@.+\\..+/.test(value) && 'Must be a valid email',\n\n  url: () => (value: Value): ErrorMessage =>\n    !!value &&\n    // eslint-disable-next-line no-useless-escape\n    !/^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$/.test(value) &&\n    'Must be a valid URL',\n};\n\nconst isNilOrEmptyString = (value: Value): boolean =>\n  value === undefined || value === null || value === '';\n\nexport const generateErrors = (\n  fieldValues: FieldValues,\n  fieldValidators: FieldValidators,\n): FieldErrors => {\n  const fieldErrors: FieldErrors = {};\n\n  Object.entries(fieldValidators).forEach(([fieldName, validators]) => {\n    [validators].flat().forEach(validator => {\n      const errorMessage = validator(fieldValues[fieldName], fieldValues);\n\n      if (errorMessage !== false && !fieldErrors[fieldName]) {\n        fieldErrors[fieldName] = errorMessage;\n      }\n    });\n  });\n  return fieldErrors;\n};\n\nexport default is;\n"
  },
  {
    "path": "api/tsconfig-paths.js",
    "content": "const tsConfigPaths = require('tsconfig-paths');\n\nconst tsConfig = require('./tsconfig.json');\n\n// Typescript compiler doesn't rewrite absolute paths back to relative\n// when compiling production code to /build. Instead we have to use\n// tsconfig-paths to do that job when we run our production start script.\n// https://github.com/microsoft/TypeScript/issues/10866\ntsConfigPaths.register({\n  baseUrl: tsConfig.compilerOptions.outDir,\n  paths: tsConfig.compilerOptions.paths,\n});\n"
  },
  {
    "path": "api/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es6\",\n    \"module\": \"commonjs\",\n    \"lib\": [\"dom\", \"es6\", \"es2017\", \"es2019\", \"esnext.asynciterable\"],\n    \"sourceMap\": true,\n    \"outDir\": \"./build\",\n    \"removeComments\": true,\n    \"strict\": true,\n    \"noImplicitAny\": true,\n    \"strictNullChecks\": true,\n    \"strictFunctionTypes\": true,\n    \"strictBindCallApply\": true,\n    \"strictPropertyInitialization\": false,\n    \"noImplicitThis\": true,\n    \"alwaysStrict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": false,\n    \"noFallthroughCasesInSwitch\": true,\n    \"moduleResolution\": \"node\",\n    \"baseUrl\": \"src\",\n    \"paths\": {\n      \"*\": [\"./*\"]\n    },\n    \"types\": [\"node\"],\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true,\n    \"forceConsistentCasingInFileNames\": true\n  },\n  \"exclude\": [\"node_modules\"],\n  \"include\": [\"./src/**/*.ts\"]\n}\n"
  },
  {
    "path": "client/.babelrc",
    "content": "{\n  \"presets\": [\n    [\n      \"@babel/preset-env\",\n      {\n        \"useBuiltIns\": \"entry\",\n        \"corejs\": 3\n      }\n    ],\n    \"@babel/react\"\n  ],\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", { \"legacy\": true }],\n    \"@babel/plugin-proposal-export-namespace-from\",\n    \"@babel/plugin-syntax-dynamic-import\",\n    [\"@babel/plugin-proposal-class-properties\", { \"loose\": true }]\n  ]\n}\n"
  },
  {
    "path": "client/.eslintrc.json",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"parserOptions\": {\n    \"sourceType\": \"module\",\n    \"ecmaVersion\": 8,\n    \"ecmaFeatures\": {\n      \"jsx\": true\n    }\n  },\n  \"env\": {\n    \"browser\": true,\n    \"jest\": true\n  },\n  \"extends\": [\"airbnb\", \"prettier\", \"prettier/react\"],\n  \"plugins\": [\"react-hooks\"],\n  \"rules\": {\n    \"react-hooks/rules-of-hooks\": \"error\",\n    \"react-hooks/exhaustive-deps\": \"warn\",\n    \"radix\": 0,\n    \"no-restricted-syntax\": 0,\n    \"no-await-in-loop\": 0,\n    \"no-console\": 0,\n    \"consistent-return\": 0,\n    \"no-param-reassign\": [2, { \"props\": false }],\n    \"no-return-assign\": [2, \"except-parens\"],\n    \"no-use-before-define\": 0,\n    \"import/prefer-default-export\": 0,\n    \"import/no-cycle\": 0,\n    \"react/no-array-index-key\": 0,\n    \"react/forbid-prop-types\": 0,\n    \"react/prop-types\": [2, { \"skipUndeclared\": true }],\n    \"react/jsx-fragments\": [2, \"element\"],\n    \"react/state-in-constructor\": 0,\n    \"react/jsx-props-no-spreading\": 0,\n    \"jsx-a11y/click-events-have-key-events\": 0\n  },\n  \"settings\": {\n    // Allows us to lint absolute imports within codebase\n    \"import/resolver\": {\n      \"node\": {\n        \"moduleDirectory\": [\"node_modules\", \"src/\"]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/.prettierrc",
    "content": "{\n  \"printWidth\": 100,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "client/README.md",
    "content": "# Project structure 🏗\n\nI've used this architecture on multiple larger projects in the past and it performed really well.\n\nThere 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.\n\nThe 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.\n\n<br>\n\n| File or folder   | Description                                                                                                                                                                                          |\n| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `src/index.jsx`  | The entry file. This is where we import babel polyfills and render the App into the root DOM node.                                                                                                   |\n| `src/index.html` | The only HTML file in our App. All scripts and styles will be injected here by Webpack.                                                                                                              |\n| `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. |\n| `src/Auth`       | Authentication module                                                                                                                                                                                |\n| `src/Project`    | Project module                                                                                                                                                                                       |\n| `src/shared`     | Components, constants, utils, hooks, styles etc. that can be used anywhere in the codebase. Any module is allowed to import from shared.                                                             |\n"
  },
  {
    "path": "client/cypress/.eslintrc.json",
    "content": "{\n  \"extends\": [\"plugin:cypress/recommended\"],\n  \"rules\": {\n    \"no-unused-expressions\": 0 // chai assertions trigger this rule\n  }\n}\n"
  },
  {
    "path": "client/cypress/integration/authentication.spec.js",
    "content": "import { testid } from '../support/utils';\n\ndescribe('Authentication', () => {\n  beforeEach(() => {\n    cy.resetDatabase();\n    cy.visit('/');\n  });\n\n  it('creates guest account if user has no auth token', () => {\n    cy.window()\n      .its('localStorage.authToken')\n      .should('be.undefined');\n\n    cy.window()\n      .its('localStorage.authToken')\n      .should('be.a', 'string')\n      .and('not.be.empty');\n\n    cy.get(testid`list-issue`).should('have.length', 8);\n  });\n});\n"
  },
  {
    "path": "client/cypress/integration/issueCreate.spec.js",
    "content": "import { testid } from '../support/utils';\n\ndescribe('Issue create', () => {\n  beforeEach(() => {\n    cy.resetDatabase();\n    cy.createTestAccount();\n    cy.visit('/project/settings?modal-issue-create=true');\n  });\n\n  it('validates form and creates issue successfully', () => {\n    cy.get(testid`modal:issue-create`).within(() => {\n      cy.get('button[type=\"submit\"]').click();\n      cy.get(testid`form-field:title`).should('contain', 'This field is required');\n\n      cy.selectOption('type', 'Story');\n      cy.get('input[name=\"title\"]').type('TEST_TITLE');\n      cy.get('.ql-editor').type('TEST_DESCRIPTION');\n      cy.selectOption('reporterId', 'Yoda');\n      cy.selectOption('userIds', 'Gaben', 'Yoda');\n      cy.selectOption('priority', 'High');\n\n      cy.get('button[type=\"submit\"]').click();\n    });\n\n    cy.get(testid`modal:issue-create`).should('not.exist');\n    cy.contains('Issue has been successfully created.').should('exist');\n    cy.location('pathname').should('equal', '/project/board');\n    cy.location('search').should('be.empty');\n\n    cy.contains(testid`list-issue`, 'TEST_TITLE')\n      .should('have.descendants', testid`avatar:Gaben`)\n      .and('have.descendants', testid`avatar:Yoda`)\n      .and('have.descendants', testid`icon:story`);\n  });\n});\n"
  },
  {
    "path": "client/cypress/integration/issueDetails.spec.js",
    "content": "import { testid } from '../support/utils';\n\ndescribe('Issue details', () => {\n  beforeEach(() => {\n    cy.resetDatabase();\n    cy.createTestAccount();\n    cy.visit('/project/board');\n    getListIssue().click(); // open issue details modal\n  });\n\n  it('updates type, status, assignees, reporter, priority successfully', () => {\n    getIssueDetailsModal().within(() => {\n      cy.selectOption('type', 'Story');\n      cy.selectOption('status', 'Done');\n      cy.selectOption('assignees', 'Gaben', 'Yoda');\n      cy.selectOption('reporter', 'Yoda');\n      cy.selectOption('priority', 'Medium');\n    });\n\n    cy.assertReloadAssert(() => {\n      getIssueDetailsModal().within(() => {\n        cy.selectShouldContain('type', 'Story');\n        cy.selectShouldContain('status', 'Done');\n        cy.selectShouldContain('assignees', 'Gaben', 'Yoda');\n        cy.selectShouldContain('reporter', 'Yoda');\n        cy.selectShouldContain('priority', 'Medium');\n      });\n\n      getListIssue()\n        .should('have.descendants', testid`avatar:Gaben`)\n        .and('have.descendants', testid`avatar:Yoda`)\n        .and('have.descendants', testid`icon:story`);\n    });\n  });\n\n  it('updates title, description successfully', () => {\n    getIssueDetailsModal().within(() => {\n      cy.get('textarea[placeholder=\"Short summary\"]')\n        .clear()\n        .type('TEST_TITLE')\n        .blur();\n\n      cy.contains('Add a description...')\n        .click()\n        .should('not.exist');\n\n      cy.get('.ql-editor').type('TEST_DESCRIPTION');\n\n      cy.contains('button', 'Save')\n        .click()\n        .should('not.exist');\n    });\n\n    cy.assertReloadAssert(() => {\n      getIssueDetailsModal().within(() => {\n        cy.get('textarea[placeholder=\"Short summary\"]').should('have.value', 'TEST_TITLE');\n        cy.get('.ql-editor').should('contain', 'TEST_DESCRIPTION');\n      });\n\n      cy.get(testid`list-issue`).should('contain', 'TEST_TITLE');\n    });\n  });\n\n  it('updates estimate, time tracking successfully', () => {\n    getIssueDetailsModal().within(() => {\n      getNumberInputAtIndex(0).debounced('type', '10');\n      cy.contains('10h estimated').click(); // open tracking modal\n    });\n\n    cy.get(testid`modal:tracking`).within(() => {\n      cy.contains('No time logged').should('exist');\n\n      getNumberInputAtIndex(0).debounced('type', 1);\n      cy.get('div[width=\"10\"]').should('exist'); // tracking bar\n\n      getNumberInputAtIndex(1).debounced('type', 2);\n\n      cy.contains('button', 'Done')\n        .click()\n        .should('not.exist');\n    });\n\n    cy.assertReloadAssert(() => {\n      getIssueDetailsModal().within(() => {\n        getNumberInputAtIndex(0).should('have.value', '10');\n        cy.contains('1h logged').should('exist');\n        cy.contains('2h remaining').should('exist');\n        cy.get('div[width*=\"33.3333\"]').should('exist');\n      });\n    });\n  });\n\n  it('deletes an issue successfully', () => {\n    getIssueDetailsModal()\n      .find(`button ${testid`icon:trash`}`)\n      .click();\n\n    cy.get(testid`modal:confirm`)\n      .contains('button', 'Delete issue')\n      .click();\n\n    cy.assertReloadAssert(() => {\n      getIssueDetailsModal().should('not.exist');\n      getListIssue().should('not.exist');\n    });\n  });\n\n  it('creates a comment successfully', () => {\n    getIssueDetailsModal().within(() => {\n      cy.contains('Add a comment...')\n        .click()\n        .should('not.exist');\n\n      cy.get('textarea[placeholder=\"Add a comment...\"]').type('TEST_COMMENT');\n\n      cy.contains('button', 'Save')\n        .click()\n        .should('not.exist');\n\n      cy.contains('Add a comment...').should('exist');\n      cy.get(testid`issue-comment`).should('contain', 'TEST_COMMENT');\n    });\n  });\n\n  it('edits a comment successfully', () => {\n    getIssueDetailsModal().within(() => {\n      cy.get(testid`issue-comment`)\n        .contains('Edit')\n        .click()\n        .should('not.exist');\n\n      cy.get('textarea[placeholder=\"Add a comment...\"]')\n        .should('have.value', 'Comment body')\n        .clear()\n        .type('TEST_COMMENT_EDITED');\n\n      cy.contains('button', 'Save')\n        .click()\n        .should('not.exist');\n\n      cy.get(testid`issue-comment`)\n        .should('contain', 'Edit')\n        .and('contain', 'TEST_COMMENT_EDITED');\n    });\n  });\n\n  it('deletes a comment successfully', () => {\n    getIssueDetailsModal()\n      .find(testid`issue-comment`)\n      .contains('Delete')\n      .click();\n\n    cy.get(testid`modal:confirm`)\n      .contains('button', 'Delete comment')\n      .click()\n      .should('not.exist');\n\n    getIssueDetailsModal()\n      .find(testid`issue-comment`)\n      .should('not.exist');\n  });\n\n  const getIssueDetailsModal = () => cy.get(testid`modal:issue-details`);\n  const getListIssue = () => cy.contains(testid`list-issue`, 'Issue title 1');\n  const getNumberInputAtIndex = index => cy.get('input[placeholder=\"Number\"]').eq(index);\n});\n"
  },
  {
    "path": "client/cypress/integration/issueFilters.spec.js",
    "content": "import { testid } from '../support/utils';\n\ndescribe('Issue filters', () => {\n  beforeEach(() => {\n    cy.resetDatabase();\n    cy.createTestAccount();\n    cy.visit('/project/board');\n  });\n\n  it('filters issues', () => {\n    getSearchInput().debounced('type', 'Issue title 1');\n    assertIssuesCount(1);\n    getSearchInput().debounced('clear');\n    assertIssuesCount(3);\n\n    getUserAvatar().click();\n    assertIssuesCount(2);\n    getUserAvatar().click();\n    assertIssuesCount(3);\n\n    getMyOnlyButton().click();\n    assertIssuesCount(2);\n    getMyOnlyButton().click();\n    assertIssuesCount(3);\n\n    getRecentButton().click();\n    assertIssuesCount(3);\n  });\n\n  const getSearchInput = () => cy.get(testid`board-filters`).find('input');\n  const getUserAvatar = () => cy.get(testid`board-filters`).find(testid`avatar:Gaben`);\n  const getMyOnlyButton = () => cy.get(testid`board-filters`).contains('Only My Issues');\n  const getRecentButton = () => cy.get(testid`board-filters`).contains('Recently Updated');\n  const assertIssuesCount = count => cy.get(testid`list-issue`).should('have.length', count);\n});\n"
  },
  {
    "path": "client/cypress/integration/issueSearch.spec.js",
    "content": "import { testid } from '../support/utils';\n\ndescribe('Issue search', () => {\n  beforeEach(() => {\n    cy.resetDatabase();\n    cy.createTestAccount();\n    cy.visit('/project/board?modal-issue-search=true');\n  });\n\n  it('displays recent issues if search input is empty', () => {\n    getIssueSearchModal().within(() => {\n      cy.contains('Recent Issues').should('exist');\n      getIssues().should('have.length', 3);\n\n      cy.get('input').debounced('type', 'anything');\n      cy.contains('Recent Issues').should('not.exist');\n\n      cy.get('input').debounced('clear');\n      cy.contains('Recent Issues').should('exist');\n      getIssues().should('have.length', 3);\n    });\n  });\n\n  it('displays matching issues successfully', () => {\n    getIssueSearchModal().within(() => {\n      cy.get('input').debounced('type', 'Issue');\n      getIssues().should('have.length', 3);\n\n      cy.get('input').debounced('type', ' description');\n      getIssues().should('have.length', 2);\n\n      cy.get('input').debounced('type', ' 3');\n      getIssues().should('have.length', 1);\n\n      cy.contains('Matching Issues').should('exist');\n    });\n  });\n\n  it('displays message if no results were found', () => {\n    getIssueSearchModal().within(() => {\n      cy.get('input').debounced('type', 'gibberish');\n\n      getIssues().should('not.exist');\n      cy.contains(\"We couldn't find anything matching your search\").should('exist');\n    });\n  });\n\n  const getIssueSearchModal = () => cy.get(testid`modal:issue-search`);\n  const getIssues = () => cy.get('a[href*=\"/project/board/issues/\"]');\n});\n"
  },
  {
    "path": "client/cypress/integration/issuesDragDrop.spec.js",
    "content": "import { KeyCodes } from 'shared/constants/keyCodes';\n\nimport { testid } from '../support/utils';\n\ndescribe('Issues drag & drop', () => {\n  beforeEach(() => {\n    cy.resetDatabase();\n    cy.createTestAccount();\n    cy.visit('/project/board');\n  });\n\n  it('moves issue between different lists', () => {\n    cy.get(testid`board-list:backlog`).should('contain', firstIssueTitle);\n    cy.get(testid`board-list:selected`).should('not.contain', firstIssueTitle);\n    moveFirstIssue(KeyCodes.ARROW_RIGHT);\n\n    cy.assertReloadAssert(() => {\n      cy.get(testid`board-list:backlog`).should('not.contain', firstIssueTitle);\n      cy.get(testid`board-list:selected`).should('contain', firstIssueTitle);\n    });\n  });\n\n  it('moves issue within a single list', () => {\n    getIssueAtIndex(0).should('contain', firstIssueTitle);\n    getIssueAtIndex(1).should('contain', secondIssueTitle);\n    moveFirstIssue(KeyCodes.ARROW_DOWN);\n\n    cy.assertReloadAssert(() => {\n      getIssueAtIndex(0).should('contain', secondIssueTitle);\n      getIssueAtIndex(1).should('contain', firstIssueTitle);\n    });\n  });\n\n  const firstIssueTitle = 'Issue title 1';\n  const secondIssueTitle = 'Issue title 2';\n\n  const getIssueAtIndex = index => cy.get(testid`list-issue`).eq(index);\n\n  const moveFirstIssue = directionKeyCode => {\n    cy.waitForXHR('PUT', '/issues/**', () => {\n      getIssueAtIndex(0)\n        .focus()\n        .trigger('keydown', { keyCode: KeyCodes.SPACE })\n        .trigger('keydown', { keyCode: directionKeyCode, force: true })\n        .trigger('keydown', { keyCode: KeyCodes.SPACE, force: true });\n    });\n  };\n});\n"
  },
  {
    "path": "client/cypress/integration/projectSettings.spec.js",
    "content": "import { testid } from '../support/utils';\n\ndescribe('Project settings', () => {\n  beforeEach(() => {\n    cy.resetDatabase();\n    cy.createTestAccount();\n    cy.visit('/project/settings');\n  });\n\n  it('should display current values in form', () => {\n    cy.get('input[name=\"name\"]').should('have.value', 'Project name');\n    cy.get('input[name=\"url\"]').should('have.value', 'https://www.testurl.com');\n    cy.get('.ql-editor').should('contain', 'Project description');\n    cy.selectShouldContain('category', 'Software');\n  });\n\n  it('validates form and updates project successfully', () => {\n    cy.get('input[name=\"name\"]').clear();\n    cy.get('button[type=\"submit\"]').click();\n    cy.get(testid`form-field:name`).should('contain', 'This field is required');\n\n    cy.get('input[name=\"name\"]').type('TEST_NAME');\n    cy.get(testid`form-field:name`).should('not.contain', 'This field is required');\n\n    cy.selectOption('category', 'Business');\n    cy.get('button[type=\"submit\"]').click();\n    cy.contains('Changes have been saved successfully.').should('exist');\n\n    cy.reload();\n\n    cy.get('input[name=\"name\"]').should('have.value', 'TEST_NAME');\n    cy.selectShouldContain('category', 'Business');\n  });\n});\n"
  },
  {
    "path": "client/cypress/plugins/index.js",
    "content": "/* eslint-disable global-require */\n/* eslint-disable import/no-extraneous-dependencies */\n\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\nconst webpack = require('@cypress/webpack-preprocessor');\nconst webpackOptions = require('../../webpack.config.js');\n\nmodule.exports = on => {\n  on('file:preprocessor', webpack({ webpackOptions }));\n};\n"
  },
  {
    "path": "client/cypress/support/commands.js",
    "content": "import 'core-js/stable';\nimport 'regenerator-runtime/runtime';\n\nimport '@4tw/cypress-drag-drop';\n\nimport { objectToQueryString } from 'shared/utils/url';\nimport { getStoredAuthToken, storeAuthToken } from 'shared/utils/authToken';\n\nimport { testid } from './utils';\n\nCypress.Commands.add('selectOption', (selectName, ...optionLabels) => {\n  optionLabels.forEach(optionLabel => {\n    cy.get(testid`select:${selectName}`).click('bottomRight');\n    cy.get(testid`select-option:${optionLabel}`).click();\n  });\n});\n\nCypress.Commands.add('selectShouldContain', (selectName, ...optionLabels) => {\n  optionLabels.forEach(optionLabel => {\n    cy.get(testid`select:${selectName}`).should('contain', optionLabel);\n  });\n});\n\n// We don't want to waste time when running tests on cypress waiting for debounced\n// inputs. We can use tick() to speed up time and trigger onChange immediately.\nCypress.Commands.add('debounced', { prevSubject: true }, (input, action, value) => {\n  cy.clock();\n  cy.wrap(input)[action](value);\n  cy.tick(1000);\n});\n\n// Sometimes cypress fails to properly wait for api requests to finish which results\n// in flaky tests, and in those cases we need to explicitly tell it to wait\n// https://docs.cypress.io/guides/guides/network-requests.html#Flake\nCypress.Commands.add('waitForXHR', (method, url, funcThatTriggersXHR) => {\n  const alias = method + url;\n  cy.server();\n  cy.route(method, url).as(alias);\n  funcThatTriggersXHR();\n  cy.wait(`@${alias}`);\n});\n\n// We're using optimistic updates (not waiting for API response before updating\n// the local data and UI) in a lot of places in the app. That's why we want to assert\n// both the immediate local UI change in the first assert, and if the change was\n// successfully persisted by the API in the second assert after page reload\nCypress.Commands.add('assertReloadAssert', assertFunc => {\n  assertFunc();\n  cy.reload();\n  assertFunc();\n});\n\nCypress.Commands.add('apiRequest', (method, url, variables = {}, options = {}) => {\n  cy.request({\n    method,\n    url: `${Cypress.env('apiBaseUrl')}${url}`,\n    qs: method === 'GET' ? objectToQueryString(variables) : undefined,\n    body: method !== 'GET' ? variables : undefined,\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: getStoredAuthToken() ? `Bearer ${getStoredAuthToken()}` : undefined,\n    },\n    ...options,\n  });\n});\n\nCypress.Commands.add('resetDatabase', () => {\n  cy.apiRequest('DELETE', '/test/reset-database');\n});\n\nCypress.Commands.add('createTestAccount', () => {\n  cy.apiRequest('POST', '/test/create-account').then(response => {\n    storeAuthToken(response.body.authToken);\n  });\n});\n"
  },
  {
    "path": "client/cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\nimport './commands';\n"
  },
  {
    "path": "client/cypress/support/utils.js",
    "content": "export const testid = (strings, ...values) => {\n  const id = strings.map((str, index) => str + (values[index] || '')).join('');\n  return `[data-testid=\"${id}\"]`;\n};\n"
  },
  {
    "path": "client/cypress.json",
    "content": "{\n  \"baseUrl\": \"http://localhost:8080\",\n  \"viewportHeight\": 800,\n  \"viewportWidth\": 1440,\n  \"env\": {\n    \"apiBaseUrl\": \"http://localhost:3000\"\n  }\n}\n"
  },
  {
    "path": "client/jest/fileMock.js",
    "content": "module.exports = 'test-file-stub';\n"
  },
  {
    "path": "client/jest/styleMock.js",
    "content": "module.exports = {};\n"
  },
  {
    "path": "client/jest.config.js",
    "content": "module.exports = {\n  moduleFileExtensions: ['*', 'js', 'jsx'],\n  moduleDirectories: ['src', 'node_modules'],\n  moduleNameMapper: {\n    '\\\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':\n      '<rootDir>/jest/fileMock.js',\n    '\\\\.(css|scss|less)$': '<rootDir>/jest/styleMock.js',\n  },\n};\n"
  },
  {
    "path": "client/jsconfig.json",
    "content": "// This config allows VSCode intellisense to work with absolute \"src\" imports and jsx files\n{\n  \"compilerOptions\": {\n    \"baseUrl\": \"./src\",\n    \"jsx\": \"react\"\n  },\n  \"include\": [\"src/**/*\", \"cypress/**/*.js\", \"./node_modules/cypress\"]\n}\n"
  },
  {
    "path": "client/package.json",
    "content": "{\n  \"name\": \"jira_client\",\n  \"version\": \"1.0.0\",\n  \"author\": \"Ivor Reic\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"webpack-dev-server\",\n    \"start:production\": \"pm2 start --name 'jira_client' server.js\",\n    \"test:jest\": \"jest\",\n    \"test:cypress\": \"node_modules/.bin/cypress open\",\n    \"build\": \"rm -rf build && webpack --config webpack.config.production.js --progress\",\n    \"pre-commit\": \"lint-staged\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.7.4\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.7.4\",\n    \"@babel/plugin-proposal-decorators\": \"^7.7.4\",\n    \"@babel/plugin-proposal-export-namespace-from\": \"^7.7.4\",\n    \"@babel/plugin-syntax-dynamic-import\": \"^7.7.4\",\n    \"@babel/preset-env\": \"^7.7.4\",\n    \"@babel/preset-react\": \"^7.7.4\",\n    \"@cypress/webpack-preprocessor\": \"^4.1.1\",\n    \"babel-eslint\": \"^10.0.3\",\n    \"babel-loader\": \"^8.0.6\",\n    \"css-loader\": \"^3.3.2\",\n    \"cypress\": \"^3.8.1\",\n    \"eslint\": \"^6.1.0\",\n    \"eslint-config-airbnb\": \"^18.0.1\",\n    \"eslint-config-prettier\": \"^6.7.0\",\n    \"eslint-plugin-cypress\": \"^2.8.1\",\n    \"eslint-plugin-import\": \"^2.18.2\",\n    \"eslint-plugin-jsx-a11y\": \"^6.2.3\",\n    \"eslint-plugin-react\": \"^7.17.0\",\n    \"eslint-plugin-react-hooks\": \"^1.7.0\",\n    \"file-loader\": \"^5.0.2\",\n    \"html-webpack-plugin\": \"^3.2.0\",\n    \"jest\": \"^24.9.0\",\n    \"lint-staged\": \"^9.5.0\",\n    \"prettier\": \"^1.19.1\",\n    \"style-loader\": \"^1.0.1\",\n    \"url-loader\": \"^3.0.0\",\n    \"webpack\": \"^4.41.2\",\n    \"webpack-cli\": \"^3.3.10\",\n    \"webpack-dev-server\": \"^3.9.0\"\n  },\n  \"dependencies\": {\n    \"@4tw/cypress-drag-drop\": \"^1.3.0\",\n    \"axios\": \"^0.19.0\",\n    \"color\": \"^3.1.2\",\n    \"compression\": \"^1.7.4\",\n    \"core-js\": \"^3.4.7\",\n    \"express\": \"^4.17.1\",\n    \"express-history-api-fallback\": \"^2.2.1\",\n    \"formik\": \"^2.1.1\",\n    \"history\": \"^4.10.1\",\n    \"jwt-decode\": \"^2.2.0\",\n    \"lodash\": \"^4.17.15\",\n    \"moment\": \"^2.24.0\",\n    \"prop-types\": \"^15.7.2\",\n    \"query-string\": \"^6.9.0\",\n    \"quill\": \"^1.3.7\",\n    \"react\": \"^16.12.0\",\n    \"react-beautiful-dnd\": \"^12.2.0\",\n    \"react-content-loader\": \"^4.3.3\",\n    \"react-dom\": \"^16.12.0\",\n    \"react-router-dom\": \"^5.1.2\",\n    \"react-textarea-autosize\": \"^7.1.2\",\n    \"react-transition-group\": \"^4.3.0\",\n    \"regenerator-runtime\": \"^0.13.3\",\n    \"styled-components\": \"^4.4.1\",\n    \"sweet-pubsub\": \"^1.1.2\"\n  },\n  \"lint-staged\": {\n    \"*.{js,jsx}\": [\n      \"eslint --fix\",\n      \"prettier --write\",\n      \"git add\"\n    ]\n  }\n}\n"
  },
  {
    "path": "client/server.js",
    "content": "const express = require('express');\nconst fallback = require('express-history-api-fallback');\nconst compression = require('compression');\n\nconst app = express();\n\napp.use(compression());\n\napp.use(express.static(`${__dirname}/build`));\n\napp.use(fallback(`${__dirname}/build/index.html`));\n\napp.listen(process.env.PORT || 8081);\n"
  },
  {
    "path": "client/src/App/BaseStyles.js",
    "content": "import { createGlobalStyle } from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\n\nexport default createGlobalStyle`\n  html, body, #root {\n    height: 100%;\n    min-height: 100%;\n    min-width: 768px;\n  }\n\n  body {\n    color: ${color.textDarkest};\n    -webkit-tap-highlight-color: transparent;\n    line-height: 1.2;\n    ${font.size(16)}\n    ${font.regular}\n  }\n\n  #root {\n    display: flex;\n    flex-direction: column;\n  }\n\n  button,\n  input,\n  optgroup,\n  select,\n  textarea {\n    ${font.regular}\n  }\n\n  *, *:after, *:before, input[type=\"search\"] {\n    box-sizing: border-box;\n  }\n\n  a {\n    color: inherit;\n    text-decoration: none;\n  }\n\n  ul {\n    list-style: none;\n  }\n\n  ul, li, ol, dd, h1, h2, h3, h4, h5, h6, p {\n    padding: 0;\n    margin: 0;\n  }\n\n  h1, h2, h3, h4, h5, h6, strong {\n    ${font.bold}\n  }\n\n  button {\n    background: none;\n    border: none;\n  }\n\n  /* Workaround for IE11 focus highlighting for select elements */\n  select::-ms-value {\n    background: none;\n    color: #42413d;\n  }\n\n  [role=\"button\"], button, input, select, textarea {\n    outline: none;\n    &:focus {\n      outline: none;\n    }\n    &:disabled {\n      opacity: 1;\n    }\n  }\n  [role=\"button\"], button, input, textarea {\n    appearance: none;\n  }\n  select:-moz-focusring {\n    color: transparent;\n    text-shadow: 0 0 0 #000;\n  }\n  select::-ms-expand {\n    display: none;\n  }\n  select option {\n    color: ${color.textDarkest};\n  }\n\n  p {\n    line-height: 1.4285;\n    a {\n      ${mixin.link()}\n    }\n  }\n\n  textarea {\n    line-height: 1.4285;\n  }\n\n  body, select {\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n  }\n\n  html {\n    touch-action: manipulation;\n  }\n\n  ${mixin.placeholderColor(color.textLight)}\n`;\n"
  },
  {
    "path": "client/src/App/NormalizeStyles.js",
    "content": "import { createGlobalStyle } from 'styled-components';\n\n/** DO NOT ALTER THIS FILE. It is a copy of https://necolas.github.io/normalize.css/ */\n\nexport default createGlobalStyle`\n  html {\n    line-height: 1.15;\n    -webkit-text-size-adjust: 100%;\n  }\n  body {\n    margin: 0;\n  }\n  main {\n    display: block;\n  }\n  h1 {\n    font-size: 2em;\n    margin: 0.67em 0;\n  }\n  hr {\n    box-sizing: content-box;\n    height: 0;\n    overflow: visible;\n  }\n  pre {\n    font-family: monospace, monospace;\n    font-size: 1em;\n  }\n  a {\n    background-color: transparent;\n  }\n  abbr[title] {\n    border-bottom: none;\n    text-decoration: underline;\n    text-decoration: underline dotted;\n  }\n  b,\n  strong {\n    font-weight: bolder;\n  }\n  code,\n  kbd,\n  samp {\n    font-family: monospace, monospace;\n    font-size: 1em;\n  }\n  small {\n    font-size: 80%;\n  }\n  sub,\n  sup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline;\n  }\n  sub {\n    bottom: -0.25em;\n  }\n  sup {\n    top: -0.5em;\n  }\n  img {\n    border-style: none;\n  }\n  button,\n  input,\n  optgroup,\n  select,\n  textarea {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    margin: 0;\n  }\n  button,\n  input {\n    overflow: visible;\n  }\n  button,\n  select {\n    text-transform: none;\n  }\n  button,\n  [type=\"button\"],\n  [type=\"reset\"],\n  [type=\"submit\"] {\n    -webkit-appearance: button;\n  }\n  button::-moz-focus-inner,\n  [type=\"button\"]::-moz-focus-inner,\n  [type=\"reset\"]::-moz-focus-inner,\n  [type=\"submit\"]::-moz-focus-inner {\n    border-style: none;\n    padding: 0;\n  }\n  button:-moz-focusring,\n  [type=\"button\"]:-moz-focusring,\n  [type=\"reset\"]:-moz-focusring,\n  [type=\"submit\"]:-moz-focusring {\n    outline: 1px dotted ButtonText;\n  }\n  fieldset {\n    padding: 0.35em 0.75em 0.625em;\n  }\n  legend {\n    box-sizing: border-box;\n    color: inherit;\n    display: table;\n    max-width: 100%;\n    padding: 0;\n    white-space: normal;\n  }\n  progress {\n    vertical-align: baseline;\n  }\n  textarea {\n    overflow: auto;\n  }\n  [type=\"checkbox\"],\n  [type=\"radio\"] {\n    box-sizing: border-box;\n    padding: 0;\n  }\n  [type=\"number\"]::-webkit-inner-spin-button,\n  [type=\"number\"]::-webkit-outer-spin-button {\n    height: auto;\n  }\n  [type=\"search\"] {\n    -webkit-appearance: textfield;\n    outline-offset: -2px;\n  }\n  [type=\"search\"]::-webkit-search-decoration {\n    -webkit-appearance: none;\n  }\n  ::-webkit-file-upload-button {\n    -webkit-appearance: button;\n    font: inherit;\n  }\n  details {\n    display: block;\n  }\n  summary {\n    display: list-item;\n  }\n  template {\n    display: none;\n  }\n  [hidden] {\n    display: none;\n  }\n`;\n"
  },
  {
    "path": "client/src/App/Routes.jsx",
    "content": "import React from 'react';\nimport { Router, Switch, Route, Redirect } from 'react-router-dom';\n\nimport history from 'browserHistory';\nimport Project from 'Project';\nimport Authenticate from 'Auth/Authenticate';\nimport PageError from 'shared/components/PageError';\n\nconst Routes = () => (\n  <Router history={history}>\n    <Switch>\n      <Redirect exact from=\"/\" to=\"/project\" />\n      <Route path=\"/authenticate\" component={Authenticate} />\n      <Route path=\"/project\" component={Project} />\n      <Route component={PageError} />\n    </Switch>\n  </Router>\n);\n\nexport default Routes;\n"
  },
  {
    "path": "client/src/App/Toast/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font, mixin, zIndexValues } from 'shared/utils/styles';\nimport { Icon } from 'shared/components';\n\nexport const Container = styled.div`\n  z-index: ${zIndexValues.modal + 1};\n  position: fixed;\n  right: 30px;\n  top: 50px;\n`;\n\nexport const StyledToast = styled.div`\n  position: relative;\n  margin-bottom: 5px;\n  width: 300px;\n  padding: 15px 20px;\n  border-radius: 3px;\n  color: #fff;\n  background: ${props => color[props.type]};\n  cursor: pointer;\n  transition: all 0.15s;\n  ${mixin.clearfix}\n  ${mixin.hardwareAccelerate}\n\n  &.jira-toast-enter,\n  &.jira-toast-exit.jira-toast-exit-active {\n    opacity: 0;\n    right: -10px;\n  }\n\n  &.jira-toast-exit,\n  &.jira-toast-enter.jira-toast-enter-active {\n    opacity: 1;\n    right: 0;\n  }\n`;\n\nexport const CloseIcon = styled(Icon)`\n  position: absolute;\n  top: 13px;\n  right: 14px;\n  font-size: 22px;\n  cursor: pointer;\n  color: #fff;\n`;\n\nexport const Title = styled.div`\n  padding-right: 22px;\n  ${font.size(15)}\n  ${font.medium}\n`;\n\nexport const Message = styled.div`\n  padding: 8px 10px 0 0;\n  white-space: pre-wrap;\n  ${font.size(14)}\n  ${font.medium}\n`;\n"
  },
  {
    "path": "client/src/App/Toast/index.jsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { CSSTransition, TransitionGroup } from 'react-transition-group';\nimport pubsub from 'sweet-pubsub';\nimport { uniqueId } from 'lodash';\n\nimport { Container, StyledToast, CloseIcon, Title, Message } from './Styles';\n\nconst Toast = () => {\n  const [toasts, setToasts] = useState([]);\n\n  useEffect(() => {\n    const addToast = ({ type = 'success', title, message, duration = 5 }) => {\n      const id = uniqueId('toast-');\n\n      setToasts(currentToasts => [...currentToasts, { id, type, title, message }]);\n\n      if (duration) {\n        setTimeout(() => removeToast(id), duration * 1000);\n      }\n    };\n\n    pubsub.on('toast', addToast);\n\n    return () => {\n      pubsub.off('toast', addToast);\n    };\n  }, []);\n\n  const removeToast = id => {\n    setToasts(currentToasts => currentToasts.filter(toast => toast.id !== id));\n  };\n\n  return (\n    <Container>\n      <TransitionGroup>\n        {toasts.map(toast => (\n          <CSSTransition key={toast.id} classNames=\"jira-toast\" timeout={200}>\n            <StyledToast key={toast.id} type={toast.type} onClick={() => removeToast(toast.id)}>\n              <CloseIcon type=\"close\" />\n              {toast.title && <Title>{toast.title}</Title>}\n              {toast.message && <Message>{toast.message}</Message>}\n            </StyledToast>\n          </CSSTransition>\n        ))}\n      </TransitionGroup>\n    </Container>\n  );\n};\n\nexport default Toast;\n"
  },
  {
    "path": "client/src/App/fontStyles.css",
    "content": "@font-face {\n  font-family: 'CircularStdBlack';\n  src: url('./assets/fonts/CircularStd-Black.woff2') format('woff2'),\n    url('./assets/fonts/CircularStd-Black.woff') format('woff');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'CircularStdBold';\n  src: url('./assets/fonts/CircularStd-Bold.woff2') format('woff2'),\n    url('./assets/fonts/CircularStd-Bold.woff') format('woff');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'CircularStdMedium';\n  src: url('./assets/fonts/CircularStd-Medium.woff2') format('woff2'),\n    url('./assets/fonts/CircularStd-Medium.woff') format('woff');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'CircularStdBook';\n  src: url('./assets/fonts/CircularStd-Book.woff2') format('woff2'),\n    url('./assets/fonts/CircularStd-Book.woff') format('woff');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'jira';\n  src: url('./assets/fonts/jira.woff') format('truetype'),\n    url('./assets/fonts/jira.ttf') format('woff'), url('./assets/fonts/jira.svg#jira') format('svg');\n  font-weight: normal;\n  font-style: normal;\n}\n"
  },
  {
    "path": "client/src/App/index.jsx",
    "content": "import React, { Fragment } from 'react';\n\nimport NormalizeStyles from './NormalizeStyles';\nimport BaseStyles from './BaseStyles';\nimport Toast from './Toast';\nimport Routes from './Routes';\n\n// We're importing .css because @font-face in styled-components causes font files\n// to be constantly re-requested from the server (which causes screen flicker)\n// https://github.com/styled-components/styled-components/issues/1593\nimport './fontStyles.css';\n\nconst App = () => (\n  <Fragment>\n    <NormalizeStyles />\n    <BaseStyles />\n    <Toast />\n    <Routes />\n  </Fragment>\n);\n\nexport default App;\n"
  },
  {
    "path": "client/src/Auth/Authenticate.jsx",
    "content": "import React, { useEffect } from 'react';\nimport { useHistory } from 'react-router-dom';\n\nimport api from 'shared/utils/api';\nimport toast from 'shared/utils/toast';\nimport { getStoredAuthToken, storeAuthToken } from 'shared/utils/authToken';\nimport { PageLoader } from 'shared/components';\n\nconst Authenticate = () => {\n  const history = useHistory();\n\n  useEffect(() => {\n    const createGuestAccount = async () => {\n      try {\n        const { authToken } = await api.post('/authentication/guest');\n        storeAuthToken(authToken);\n        history.push('/');\n      } catch (error) {\n        toast.error(error);\n      }\n    };\n\n    if (!getStoredAuthToken()) {\n      createGuestAccount();\n    }\n  }, [history]);\n\n  return <PageLoader />;\n};\n\nexport default Authenticate;\n"
  },
  {
    "path": "client/src/Project/Board/Filters/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport { InputDebounced, Avatar, Button } from 'shared/components';\n\nexport const Filters = styled.div`\n  display: flex;\n  align-items: center;\n  margin-top: 24px;\n`;\n\nexport const SearchInput = styled(InputDebounced)`\n  margin-right: 18px;\n  width: 160px;\n`;\n\nexport const Avatars = styled.div`\n  display: flex;\n  flex-direction: row-reverse;\n  margin: 0 12px 0 2px;\n`;\n\nexport const AvatarIsActiveBorder = styled.div`\n  display: inline-flex;\n  margin-left: -2px;\n  border-radius: 50%;\n  transition: transform 0.1s;\n  ${mixin.clickable};\n  ${props => props.isActive && `box-shadow: 0 0 0 4px ${color.primary}`}\n  &:hover {\n    transform: translateY(-5px);\n  }\n`;\n\nexport const StyledAvatar = styled(Avatar)`\n  box-shadow: 0 0 0 2px #fff;\n`;\n\nexport const StyledButton = styled(Button)`\n  margin-left: 6px;\n`;\n\nexport const ClearAll = styled.div`\n  height: 32px;\n  line-height: 32px;\n  margin-left: 15px;\n  padding-left: 12px;\n  border-left: 1px solid ${color.borderLightest};\n  color: ${color.textDark};\n  ${font.size(14.5)}\n  ${mixin.clickable}\n  &:hover {\n    color: ${color.textMedium};\n  }\n`;\n"
  },
  {
    "path": "client/src/Project/Board/Filters/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { xor } from 'lodash';\n\nimport {\n  Filters,\n  SearchInput,\n  Avatars,\n  AvatarIsActiveBorder,\n  StyledAvatar,\n  StyledButton,\n  ClearAll,\n} from './Styles';\n\nconst propTypes = {\n  projectUsers: PropTypes.array.isRequired,\n  defaultFilters: PropTypes.object.isRequired,\n  filters: PropTypes.object.isRequired,\n  mergeFilters: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, mergeFilters }) => {\n  const { searchTerm, userIds, myOnly, recent } = filters;\n\n  const areFiltersCleared = !searchTerm && userIds.length === 0 && !myOnly && !recent;\n\n  return (\n    <Filters data-testid=\"board-filters\">\n      <SearchInput\n        icon=\"search\"\n        value={searchTerm}\n        onChange={value => mergeFilters({ searchTerm: value })}\n      />\n      <Avatars>\n        {projectUsers.map(user => (\n          <AvatarIsActiveBorder key={user.id} isActive={userIds.includes(user.id)}>\n            <StyledAvatar\n              avatarUrl={user.avatarUrl}\n              name={user.name}\n              onClick={() => mergeFilters({ userIds: xor(userIds, [user.id]) })}\n            />\n          </AvatarIsActiveBorder>\n        ))}\n      </Avatars>\n      <StyledButton\n        variant=\"empty\"\n        isActive={myOnly}\n        onClick={() => mergeFilters({ myOnly: !myOnly })}\n      >\n        Only My Issues\n      </StyledButton>\n      <StyledButton\n        variant=\"empty\"\n        isActive={recent}\n        onClick={() => mergeFilters({ recent: !recent })}\n      >\n        Recently Updated\n      </StyledButton>\n      {!areFiltersCleared && (\n        <ClearAll onClick={() => mergeFilters(defaultFilters)}>Clear all</ClearAll>\n      )}\n    </Filters>\n  );\n};\n\nProjectBoardFilters.propTypes = propTypes;\n\nexport default ProjectBoardFilters;\n"
  },
  {
    "path": "client/src/Project/Board/Header/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\n\nexport const Header = styled.div`\n  margin-top: 6px;\n  display: flex;\n  justify-content: space-between;\n`;\n\nexport const BoardName = styled.div`\n  ${font.size(24)}\n  ${font.medium}\n`;\n"
  },
  {
    "path": "client/src/Project/Board/Header/index.jsx",
    "content": "import React from 'react';\n\nimport { Button } from 'shared/components';\n\nimport { Header, BoardName } from './Styles';\n\nconst ProjectBoardHeader = () => (\n  <Header>\n    <BoardName>Kanban board</BoardName>\n    <a href=\"https://github.com/oldboyxx/jira_clone\" target=\"_blank\" rel=\"noreferrer noopener\">\n      <Button icon=\"github\">Github Repo</Button>\n    </a>\n  </Header>\n);\n\nexport default ProjectBoardHeader;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/AssigneesReporter/Styles.js",
    "content": "import styled, { css } from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\n\nexport const User = styled.div`\n  display: flex;\n  align-items: center;\n  ${mixin.clickable}\n  ${props =>\n    props.isSelectValue &&\n    css`\n      margin: 0 10px ${props.withBottomMargin ? 5 : 0}px 0;\n      padding: 4px 8px;\n      border-radius: 4px;\n      background: ${color.backgroundLight};\n      transition: background 0.1s;\n      &:hover {\n        background: ${color.backgroundMedium};\n      }\n    `}\n`;\n\nexport const Username = styled.div`\n  padding: 0 3px 0 8px;\n  ${font.size(14.5)}\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/AssigneesReporter/index.jsx",
    "content": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Avatar, Select, Icon } from 'shared/components';\n\nimport { SectionTitle } from '../Styles';\nimport { User, Username } from './Styles';\n\nconst propTypes = {\n  issue: PropTypes.object.isRequired,\n  updateIssue: PropTypes.func.isRequired,\n  projectUsers: PropTypes.array.isRequired,\n};\n\nconst ProjectBoardIssueDetailsAssigneesReporter = ({ issue, updateIssue, projectUsers }) => {\n  const getUserById = userId => projectUsers.find(user => user.id === userId);\n\n  const userOptions = projectUsers.map(user => ({ value: user.id, label: user.name }));\n\n  return (\n    <Fragment>\n      <SectionTitle>Assignees</SectionTitle>\n      <Select\n        isMulti\n        variant=\"empty\"\n        dropdownWidth={343}\n        placeholder=\"Unassigned\"\n        name=\"assignees\"\n        value={issue.userIds}\n        options={userOptions}\n        onChange={userIds => {\n          updateIssue({ userIds, users: userIds.map(getUserById) });\n        }}\n        renderValue={({ value: userId, removeOptionValue }) =>\n          renderUser(getUserById(userId), true, removeOptionValue)\n        }\n        renderOption={({ value: userId }) => renderUser(getUserById(userId), false)}\n      />\n\n      <SectionTitle>Reporter</SectionTitle>\n      <Select\n        variant=\"empty\"\n        dropdownWidth={343}\n        withClearValue={false}\n        name=\"reporter\"\n        value={issue.reporterId}\n        options={userOptions}\n        onChange={userId => updateIssue({ reporterId: userId })}\n        renderValue={({ value: userId }) => renderUser(getUserById(userId), true)}\n        renderOption={({ value: userId }) => renderUser(getUserById(userId))}\n      />\n    </Fragment>\n  );\n};\n\nconst renderUser = (user, isSelectValue, removeOptionValue) => (\n  <User\n    key={user.id}\n    isSelectValue={isSelectValue}\n    withBottomMargin={!!removeOptionValue}\n    onClick={() => removeOptionValue && removeOptionValue()}\n  >\n    <Avatar avatarUrl={user.avatarUrl} name={user.name} size={24} />\n    <Username>{user.name}</Username>\n    {removeOptionValue && <Icon type=\"close\" top={1} />}\n  </User>\n);\n\nProjectBoardIssueDetailsAssigneesReporter.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsAssigneesReporter;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/BodyForm/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { Button } from 'shared/components';\n\nexport const Actions = styled.div`\n  display: flex;\n  padding-top: 10px;\n`;\n\nexport const FormButton = styled(Button)`\n  margin-right: 6px;\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/BodyForm/index.jsx",
    "content": "import React, { Fragment, useRef } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Textarea } from 'shared/components';\n\nimport { Actions, FormButton } from './Styles';\n\nconst propTypes = {\n  value: PropTypes.string.isRequired,\n  onChange: PropTypes.func.isRequired,\n  isWorking: PropTypes.bool.isRequired,\n  onSubmit: PropTypes.func.isRequired,\n  onCancel: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsCommentsBodyForm = ({\n  value,\n  onChange,\n  isWorking,\n  onSubmit,\n  onCancel,\n}) => {\n  const $textareaRef = useRef();\n\n  const handleSubmit = () => {\n    if ($textareaRef.current.value.trim()) {\n      onSubmit();\n    }\n  };\n\n  return (\n    <Fragment>\n      <Textarea\n        autoFocus\n        placeholder=\"Add a comment...\"\n        value={value}\n        onChange={onChange}\n        ref={$textareaRef}\n      />\n      <Actions>\n        <FormButton variant=\"primary\" isWorking={isWorking} onClick={handleSubmit}>\n          Save\n        </FormButton>\n        <FormButton variant=\"empty\" onClick={onCancel}>\n          Cancel\n        </FormButton>\n      </Actions>\n    </Fragment>\n  );\n};\n\nProjectBoardIssueDetailsCommentsBodyForm.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsCommentsBodyForm;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Comment/Styles.js",
    "content": "import styled, { css } from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport { Avatar } from 'shared/components';\n\nexport const Comment = styled.div`\n  position: relative;\n  margin-top: 25px;\n  ${font.size(15)}\n`;\n\nexport const UserAvatar = styled(Avatar)`\n  position: absolute;\n  top: 0;\n  left: 0;\n`;\n\nexport const Content = styled.div`\n  padding-left: 44px;\n`;\n\nexport const Username = styled.div`\n  display: inline-block;\n  padding-right: 12px;\n  padding-bottom: 10px;\n  color: ${color.textDark};\n  ${font.medium}\n`;\n\nexport const CreatedAt = styled.div`\n  display: inline-block;\n  padding-bottom: 10px;\n  color: ${color.textDark};\n  ${font.size(14.5)}\n`;\n\nexport const Body = styled.p`\n  padding-bottom: 10px;\n  white-space: pre-wrap;\n`;\n\nconst actionLinkStyles = css`\n  display: inline-block;\n  padding: 2px 0;\n  color: ${color.textMedium};\n  ${font.size(14.5)}\n  ${mixin.clickable}\n  &:hover {\n    text-decoration: underline;\n  }\n`;\n\nexport const EditLink = styled.div`\n  margin-right: 12px;\n  ${actionLinkStyles}\n`;\n\nexport const DeleteLink = styled.div`\n  ${actionLinkStyles}\n  &:before {\n    position: relative;\n    right: 6px;\n    content: '·';\n    display: inline-block;\n  }\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Comment/index.jsx",
    "content": "import React, { Fragment, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport api from 'shared/utils/api';\nimport toast from 'shared/utils/toast';\nimport { formatDateTimeConversational } from 'shared/utils/dateTime';\nimport { ConfirmModal } from 'shared/components';\n\nimport BodyForm from '../BodyForm';\nimport {\n  Comment,\n  UserAvatar,\n  Content,\n  Username,\n  CreatedAt,\n  Body,\n  EditLink,\n  DeleteLink,\n} from './Styles';\n\nconst propTypes = {\n  comment: PropTypes.object.isRequired,\n  fetchIssue: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {\n  const [isFormOpen, setFormOpen] = useState(false);\n  const [isUpdating, setUpdating] = useState(false);\n  const [body, setBody] = useState(comment.body);\n\n  const handleCommentDelete = async () => {\n    try {\n      await api.delete(`/comments/${comment.id}`);\n      await fetchIssue();\n    } catch (error) {\n      toast.error(error);\n    }\n  };\n\n  const handleCommentUpdate = async () => {\n    try {\n      setUpdating(true);\n      await api.put(`/comments/${comment.id}`, { body });\n      await fetchIssue();\n      setUpdating(false);\n      setFormOpen(false);\n    } catch (error) {\n      toast.error(error);\n    }\n  };\n\n  return (\n    <Comment data-testid=\"issue-comment\">\n      <UserAvatar name={comment.user.name} avatarUrl={comment.user.avatarUrl} />\n      <Content>\n        <Username>{comment.user.name}</Username>\n        <CreatedAt>{formatDateTimeConversational(comment.createdAt)}</CreatedAt>\n\n        {isFormOpen ? (\n          <BodyForm\n            value={body}\n            onChange={setBody}\n            isWorking={isUpdating}\n            onSubmit={handleCommentUpdate}\n            onCancel={() => setFormOpen(false)}\n          />\n        ) : (\n          <Fragment>\n            <Body>{comment.body}</Body>\n            <EditLink onClick={() => setFormOpen(true)}>Edit</EditLink>\n            <ConfirmModal\n              title=\"Are you sure you want to delete this comment?\"\n              message=\"Once you delete, it's gone for good.\"\n              confirmText=\"Delete comment\"\n              onConfirm={handleCommentDelete}\n              renderLink={modal => <DeleteLink onClick={modal.open}>Delete</DeleteLink>}\n            />\n          </Fragment>\n        )}\n      </Content>\n    </Comment>\n  );\n};\n\nProjectBoardIssueDetailsComment.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsComment;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Create/ProTip/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const Tip = styled.div`\n  display: flex;\n  align-items: center;\n  padding-top: 8px;\n  color: ${color.textMedium};\n  ${font.size(13)}\n  strong {\n    padding-right: 4px;\n  }\n`;\n\nexport const TipLetter = styled.span`\n  position: relative;\n  top: 1px;\n  display: inline-block;\n  margin: 0 4px;\n  padding: 0 4px;\n  border-radius: 2px;\n  color: ${color.textDarkest};\n  background: ${color.backgroundMedium};\n  ${font.bold}\n  ${font.size(12)}\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Create/ProTip/index.jsx",
    "content": "import React, { useEffect } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { KeyCodes } from 'shared/constants/keyCodes';\nimport { isFocusedElementEditable } from 'shared/utils/browser';\n\nimport { Tip, TipLetter } from './Styles';\n\nconst propTypes = {\n  setFormOpen: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsCommentsCreateProTip = ({ setFormOpen }) => {\n  useEffect(() => {\n    const handleKeyDown = event => {\n      if (!isFocusedElementEditable() && event.keyCode === KeyCodes.M) {\n        event.preventDefault();\n        setFormOpen(true);\n      }\n    };\n\n    document.addEventListener('keydown', handleKeyDown);\n\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [setFormOpen]);\n\n  return (\n    <Tip>\n      <strong>Pro tip:</strong>press<TipLetter>M</TipLetter>to comment\n    </Tip>\n  );\n};\n\nProjectBoardIssueDetailsCommentsCreateProTip.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsCommentsCreateProTip;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Create/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport { Avatar } from 'shared/components';\n\nexport const Create = styled.div`\n  position: relative;\n  margin-top: 25px;\n  ${font.size(15)}\n`;\n\nexport const UserAvatar = styled(Avatar)`\n  position: absolute;\n  top: 0;\n  left: 0;\n`;\n\nexport const Right = styled.div`\n  padding-left: 44px;\n`;\n\nexport const FakeTextarea = styled.div`\n  padding: 12px 16px;\n  border-radius: 4px;\n  border: 1px solid ${color.borderLightest};\n  color: ${color.textLight};\n  ${mixin.clickable}\n  &:hover {\n    border: 1px solid ${color.borderLight};\n  }\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Create/index.jsx",
    "content": "import React, { Fragment, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport api from 'shared/utils/api';\nimport useCurrentUser from 'shared/hooks/currentUser';\nimport toast from 'shared/utils/toast';\n\nimport BodyForm from '../BodyForm';\nimport ProTip from './ProTip';\nimport { Create, UserAvatar, Right, FakeTextarea } from './Styles';\n\nconst propTypes = {\n  issueId: PropTypes.number.isRequired,\n  fetchIssue: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsCommentsCreate = ({ issueId, fetchIssue }) => {\n  const [isFormOpen, setFormOpen] = useState(false);\n  const [isCreating, setCreating] = useState(false);\n  const [body, setBody] = useState('');\n\n  const { currentUser } = useCurrentUser();\n\n  const handleCommentCreate = async () => {\n    try {\n      setCreating(true);\n      await api.post(`/comments`, { body, issueId, userId: currentUser.id });\n      await fetchIssue();\n      setFormOpen(false);\n      setCreating(false);\n      setBody('');\n    } catch (error) {\n      toast.error(error);\n    }\n  };\n\n  return (\n    <Create>\n      {currentUser && <UserAvatar name={currentUser.name} avatarUrl={currentUser.avatarUrl} />}\n      <Right>\n        {isFormOpen ? (\n          <BodyForm\n            value={body}\n            onChange={setBody}\n            isWorking={isCreating}\n            onSubmit={handleCommentCreate}\n            onCancel={() => setFormOpen(false)}\n          />\n        ) : (\n          <Fragment>\n            <FakeTextarea onClick={() => setFormOpen(true)}>Add a comment...</FakeTextarea>\n            <ProTip setFormOpen={setFormOpen} />\n          </Fragment>\n        )}\n      </Right>\n    </Create>\n  );\n};\n\nProjectBoardIssueDetailsCommentsCreate.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsCommentsCreate;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\n\nexport const Comments = styled.div`\n  padding-top: 40px;\n`;\n\nexport const Title = styled.div`\n  ${font.medium}\n  ${font.size(15)}\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Comments/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { sortByNewest } from 'shared/utils/javascript';\n\nimport Create from './Create';\nimport Comment from './Comment';\nimport { Comments, Title } from './Styles';\n\nconst propTypes = {\n  issue: PropTypes.object.isRequired,\n  fetchIssue: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsComments = ({ issue, fetchIssue }) => (\n  <Comments>\n    <Title>Comments</Title>\n    <Create issueId={issue.id} fetchIssue={fetchIssue} />\n\n    {sortByNewest(issue.comments, 'createdAt').map(comment => (\n      <Comment key={comment.id} comment={comment} fetchIssue={fetchIssue} />\n    ))}\n  </Comments>\n);\n\nProjectBoardIssueDetailsComments.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsComments;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Dates/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const Dates = styled.div`\n  margin-top: 11px;\n  padding-top: 13px;\n  line-height: 22px;\n  border-top: 1px solid ${color.borderLightest};\n  color: ${color.textMedium};\n  ${font.size(13)}\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Dates/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { formatDateTimeConversational } from 'shared/utils/dateTime';\n\nimport { Dates } from './Styles';\n\nconst propTypes = {\n  issue: PropTypes.object.isRequired,\n};\n\nconst ProjectBoardIssueDetailsDates = ({ issue }) => (\n  <Dates>\n    <div>Created at {formatDateTimeConversational(issue.createdAt)}</div>\n    <div>Updated at {formatDateTimeConversational(issue.updatedAt)}</div>\n  </Dates>\n);\n\nProjectBoardIssueDetailsDates.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsDates;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Delete.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport api from 'shared/utils/api';\nimport toast from 'shared/utils/toast';\nimport { Button, ConfirmModal } from 'shared/components';\n\nconst propTypes = {\n  issue: PropTypes.object.isRequired,\n  fetchProject: PropTypes.func.isRequired,\n  modalClose: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsDelete = ({ issue, fetchProject, modalClose }) => {\n  const handleIssueDelete = async () => {\n    try {\n      await api.delete(`/issues/${issue.id}`);\n      await fetchProject();\n      modalClose();\n    } catch (error) {\n      toast.error(error);\n    }\n  };\n\n  return (\n    <ConfirmModal\n      title=\"Are you sure you want to delete this issue?\"\n      message=\"Once you delete, it's gone for good.\"\n      confirmText=\"Delete issue\"\n      onConfirm={handleIssueDelete}\n      renderLink={modal => (\n        <Button icon=\"trash\" iconSize={19} variant=\"empty\" onClick={modal.open} />\n      )}\n    />\n  );\n};\n\nProjectBoardIssueDetailsDelete.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsDelete;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Description/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\n\nexport const Title = styled.div`\n  padding: 20px 0 6px;\n  ${font.size(15)}\n  ${font.medium}\n`;\n\nexport const EmptyLabel = styled.div`\n  margin-left: -7px;\n  padding: 7px;\n  border-radius: 3px;\n  color: ${color.textMedium}\n  transition: background 0.1s;\n  ${font.size(15)}\n  ${mixin.clickable}\n  &:hover {\n    background: ${color.backgroundLight};\n  }\n`;\n\nexport const Actions = styled.div`\n  display: flex;\n  padding-top: 12px;\n  & > button {\n    margin-right: 6px;\n  }\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Description/index.jsx",
    "content": "import React, { Fragment, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { getTextContentsFromHtmlString } from 'shared/utils/browser';\nimport { TextEditor, TextEditedContent, Button } from 'shared/components';\n\nimport { Title, EmptyLabel, Actions } from './Styles';\n\nconst propTypes = {\n  issue: PropTypes.object.isRequired,\n  updateIssue: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {\n  const [description, setDescription] = useState(issue.description);\n  const [isEditing, setEditing] = useState(false);\n\n  const handleUpdate = () => {\n    setEditing(false);\n    updateIssue({ description });\n  };\n\n  const isDescriptionEmpty = getTextContentsFromHtmlString(description).trim().length === 0;\n\n  return (\n    <Fragment>\n      <Title>Description</Title>\n      {isEditing ? (\n        <Fragment>\n          <TextEditor\n            placeholder=\"Describe the issue\"\n            defaultValue={description}\n            onChange={setDescription}\n          />\n          <Actions>\n            <Button variant=\"primary\" onClick={handleUpdate}>\n              Save\n            </Button>\n            <Button variant=\"empty\" onClick={() => setEditing(false)}>\n              Cancel\n            </Button>\n          </Actions>\n        </Fragment>\n      ) : (\n        <Fragment>\n          {isDescriptionEmpty ? (\n            <EmptyLabel onClick={() => setEditing(true)}>Add a description...</EmptyLabel>\n          ) : (\n            <TextEditedContent content={description} onClick={() => setEditing(true)} />\n          )}\n        </Fragment>\n      )}\n    </Fragment>\n  );\n};\n\nProjectBoardIssueDetailsDescription.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsDescription;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/EstimateTracking/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\n\nexport const TrackingLink = styled.div`\n  padding: 4px 4px 2px 0;\n  border-radius: 4px;\n  transition: background 0.1s;\n  ${mixin.clickable}\n  &:hover {\n    background: ${color.backgroundLight};\n  }\n`;\n\nexport const ModalContents = styled.div`\n  padding: 20px 25px 25px;\n`;\n\nexport const ModalTitle = styled.div`\n  padding-bottom: 14px;\n  ${font.medium}\n  ${font.size(20)}\n`;\n\nexport const Inputs = styled.div`\n  display: flex;\n  margin: 20px -5px 30px;\n`;\n\nexport const InputCont = styled.div`\n  margin: 0 5px;\n  width: 50%;\n`;\n\nexport const InputLabel = styled.div`\n  padding-bottom: 5px;\n  color: ${color.textMedium};\n  ${font.medium};\n  ${font.size(13)};\n`;\n\nexport const Actions = styled.div`\n  display: flex;\n  justify-content: flex-end;\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/EstimateTracking/TrackingWidget/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\nimport { Icon } from 'shared/components';\n\nexport const TrackingWidget = styled.div`\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n`;\n\nexport const WatchIcon = styled(Icon)`\n  color: ${color.textMedium};\n`;\n\nexport const Right = styled.div`\n  width: 90%;\n`;\n\nexport const BarCont = styled.div`\n  height: 5px;\n  border-radius: 4px;\n  background: ${color.backgroundMedium};\n`;\n\nexport const Bar = styled.div`\n  height: 5px;\n  border-radius: 4px;\n  background: ${color.primary};\n  transition: all 0.1s;\n  width: ${props => props.width}%;\n`;\n\nexport const Values = styled.div`\n  display: flex;\n  justify-content: space-between;\n  padding-top: 3px;\n  ${font.size(14.5)};\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/EstimateTracking/TrackingWidget/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { isNil } from 'lodash';\n\nimport { TrackingWidget, WatchIcon, Right, BarCont, Bar, Values } from './Styles';\n\nconst propTypes = {\n  issue: PropTypes.object.isRequired,\n};\n\nconst ProjectBoardIssueDetailsTrackingWidget = ({ issue }) => (\n  <TrackingWidget>\n    <WatchIcon type=\"stopwatch\" size={26} top={-1} />\n    <Right>\n      <BarCont>\n        <Bar width={calculateTrackingBarWidth(issue)} />\n      </BarCont>\n      <Values>\n        <div>{issue.timeSpent ? `${issue.timeSpent}h logged` : 'No time logged'}</div>\n        {renderRemainingOrEstimate(issue)}\n      </Values>\n    </Right>\n  </TrackingWidget>\n);\n\nconst calculateTrackingBarWidth = ({ timeSpent, timeRemaining, estimate }) => {\n  if (!timeSpent) {\n    return 0;\n  }\n  if (isNil(timeRemaining) && isNil(estimate)) {\n    return 100;\n  }\n  if (!isNil(timeRemaining)) {\n    return (timeSpent / (timeSpent + timeRemaining)) * 100;\n  }\n  if (!isNil(estimate)) {\n    return Math.min((timeSpent / estimate) * 100, 100);\n  }\n};\n\nconst renderRemainingOrEstimate = ({ timeRemaining, estimate }) => {\n  if (isNil(timeRemaining) && isNil(estimate)) {\n    return null;\n  }\n  if (!isNil(timeRemaining)) {\n    return <div>{`${timeRemaining}h remaining`}</div>;\n  }\n  if (!isNil(estimate)) {\n    return <div>{`${estimate}h estimated`}</div>;\n  }\n};\n\nProjectBoardIssueDetailsTrackingWidget.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsTrackingWidget;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/EstimateTracking/index.jsx",
    "content": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\nimport { isNil } from 'lodash';\n\nimport { InputDebounced, Modal, Button } from 'shared/components';\n\nimport TrackingWidget from './TrackingWidget';\nimport { SectionTitle } from '../Styles';\nimport {\n  TrackingLink,\n  ModalContents,\n  ModalTitle,\n  Inputs,\n  InputCont,\n  InputLabel,\n  Actions,\n} from './Styles';\n\nconst propTypes = {\n  issue: PropTypes.object.isRequired,\n  updateIssue: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsEstimateTracking = ({ issue, updateIssue }) => (\n  <Fragment>\n    <SectionTitle>Original Estimate (hours)</SectionTitle>\n    {renderHourInput('estimate', issue, updateIssue)}\n\n    <SectionTitle>Time Tracking</SectionTitle>\n    <Modal\n      testid=\"modal:tracking\"\n      width={400}\n      renderLink={modal => (\n        <TrackingLink onClick={modal.open}>\n          <TrackingWidget issue={issue} />\n        </TrackingLink>\n      )}\n      renderContent={modal => (\n        <ModalContents>\n          <ModalTitle>Time tracking</ModalTitle>\n          <TrackingWidget issue={issue} />\n          <Inputs>\n            <InputCont>\n              <InputLabel>Time spent (hours)</InputLabel>\n              {renderHourInput('timeSpent', issue, updateIssue)}\n            </InputCont>\n            <InputCont>\n              <InputLabel>Time remaining (hours)</InputLabel>\n              {renderHourInput('timeRemaining', issue, updateIssue)}\n            </InputCont>\n          </Inputs>\n          <Actions>\n            <Button variant=\"primary\" onClick={modal.close}>\n              Done\n            </Button>\n          </Actions>\n        </ModalContents>\n      )}\n    />\n  </Fragment>\n);\n\nconst renderHourInput = (fieldName, issue, updateIssue) => (\n  <InputDebounced\n    placeholder=\"Number\"\n    filter={/^\\d{0,6}$/}\n    value={isNil(issue[fieldName]) ? '' : issue[fieldName]}\n    onChange={stringValue => {\n      const value = stringValue.trim() ? Number(stringValue) : null;\n      updateIssue({ [fieldName]: value });\n    }}\n  />\n);\n\nProjectBoardIssueDetailsEstimateTracking.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsEstimateTracking;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Loader.jsx",
    "content": "import React from 'react';\nimport ContentLoader from 'react-content-loader';\n\nconst IssueDetailsLoader = () => (\n  <div style={{ padding: 40 }}>\n    <ContentLoader\n      height={260}\n      width={940}\n      speed={2}\n      primaryColor=\"#f3f3f3\"\n      secondaryColor=\"#ecebeb\"\n    >\n      <rect x=\"0\" y=\"0\" rx=\"3\" ry=\"3\" width=\"627\" height=\"24\" />\n      <rect x=\"0\" y=\"29\" rx=\"3\" ry=\"3\" width=\"506\" height=\"24\" />\n      <rect x=\"0\" y=\"77\" rx=\"3\" ry=\"3\" width=\"590\" height=\"16\" />\n      <rect x=\"0\" y=\"100\" rx=\"3\" ry=\"3\" width=\"627\" height=\"16\" />\n      <rect x=\"0\" y=\"123\" rx=\"3\" ry=\"3\" width=\"480\" height=\"16\" />\n      <rect x=\"0\" y=\"187\" rx=\"3\" ry=\"3\" width=\"370\" height=\"16\" />\n      <circle cx=\"18\" cy=\"239\" r=\"18\" />\n      <rect x=\"46\" y=\"217\" rx=\"3\" ry=\"3\" width=\"548\" height=\"42\" />\n      <rect x=\"683\" y=\"3\" rx=\"3\" ry=\"3\" width=\"135\" height=\"14\" />\n      <rect x=\"683\" y=\"33\" rx=\"3\" ry=\"3\" width=\"251\" height=\"24\" />\n      <rect x=\"683\" y=\"90\" rx=\"3\" ry=\"3\" width=\"135\" height=\"14\" />\n      <rect x=\"683\" y=\"120\" rx=\"3\" ry=\"3\" width=\"251\" height=\"24\" />\n      <rect x=\"683\" y=\"177\" rx=\"3\" ry=\"3\" width=\"135\" height=\"14\" />\n      <rect x=\"683\" y=\"207\" rx=\"3\" ry=\"3\" width=\"251\" height=\"24\" />\n    </ContentLoader>\n  </div>\n);\n\nexport default IssueDetailsLoader;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Priority/Styles.js",
    "content": "import styled, { css } from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const Priority = styled.div`\n  display: flex;\n  align-items: center;\n  ${props =>\n    props.isValue &&\n    css`\n      padding: 3px 4px 3px 0px;\n      border-radius: 4px;\n      &:hover,\n      &:focus {\n        background: ${color.backgroundLight};\n      }\n    `}\n`;\n\nexport const Label = styled.div`\n  padding: 0 3px 0 8px;\n  ${font.size(14.5)}\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Priority/index.jsx",
    "content": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { IssuePriority, IssuePriorityCopy } from 'shared/constants/issues';\nimport { Select, IssuePriorityIcon } from 'shared/components';\n\nimport { SectionTitle } from '../Styles';\nimport { Priority, Label } from './Styles';\n\nconst propTypes = {\n  issue: PropTypes.object.isRequired,\n  updateIssue: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => (\n  <Fragment>\n    <SectionTitle>Priority</SectionTitle>\n    <Select\n      variant=\"empty\"\n      withClearValue={false}\n      dropdownWidth={343}\n      name=\"priority\"\n      value={issue.priority}\n      options={Object.values(IssuePriority).map(priority => ({\n        value: priority,\n        label: IssuePriorityCopy[priority],\n      }))}\n      onChange={priority => updateIssue({ priority })}\n      renderValue={({ value: priority }) => renderPriorityItem(priority, true)}\n      renderOption={({ value: priority }) => renderPriorityItem(priority)}\n    />\n  </Fragment>\n);\n\nconst renderPriorityItem = (priority, isValue) => (\n  <Priority isValue={isValue}>\n    <IssuePriorityIcon priority={priority} />\n    <Label>{IssuePriorityCopy[priority]}</Label>\n  </Priority>\n);\n\nProjectBoardIssueDetailsPriority.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsPriority;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Status/Styles.js",
    "content": "import styled, { css } from 'styled-components';\n\nimport { issueStatusColors, issueStatusBackgroundColors, mixin } from 'shared/utils/styles';\n\nexport const Status = styled.div`\n  text-transform: uppercase;\n  transition: all 0.1s;\n  ${props => mixin.tag(issueStatusBackgroundColors[props.color], issueStatusColors[props.color])}\n  ${props =>\n    props.isValue &&\n    css`\n      padding: 0 12px;\n      height: 32px;\n      &:hover {\n        transform: scale(1.05);\n      }\n    `}\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Status/index.jsx",
    "content": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { IssueStatus, IssueStatusCopy } from 'shared/constants/issues';\nimport { Select, Icon } from 'shared/components';\n\nimport { SectionTitle } from '../Styles';\nimport { Status } from './Styles';\n\nconst propTypes = {\n  issue: PropTypes.object.isRequired,\n  updateIssue: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (\n  <Fragment>\n    <SectionTitle>Status</SectionTitle>\n    <Select\n      variant=\"empty\"\n      dropdownWidth={343}\n      withClearValue={false}\n      name=\"status\"\n      value={issue.status}\n      options={Object.values(IssueStatus).map(status => ({\n        value: status,\n        label: IssueStatusCopy[status],\n      }))}\n      onChange={status => updateIssue({ status })}\n      renderValue={({ value: status }) => (\n        <Status isValue color={status}>\n          <div>{IssueStatusCopy[status]}</div>\n          <Icon type=\"chevron-down\" size={18} />\n        </Status>\n      )}\n      renderOption={({ value: status }) => (\n        <Status color={status}>{IssueStatusCopy[status]}</Status>\n      )}\n    />\n  </Fragment>\n);\n\nProjectBoardIssueDetailsStatus.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsStatus;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const Content = styled.div`\n  display: flex;\n  padding: 0 30px 60px;\n`;\n\nexport const Left = styled.div`\n  width: 65%;\n  padding-right: 50px;\n`;\n\nexport const Right = styled.div`\n  width: 35%;\n  padding-top: 5px;\n`;\n\nexport const TopActions = styled.div`\n  display: flex;\n  justify-content: space-between;\n  padding: 21px 18px 0;\n`;\n\nexport const TopActionsRight = styled.div`\n  display: flex;\n  align-items: center;\n  & > * {\n    margin-left: 4px;\n  }\n`;\n\nexport const SectionTitle = styled.div`\n  margin: 24px 0 5px;\n  text-transform: uppercase;\n  color: ${color.textMedium};\n  ${font.size(12.5)}\n  ${font.bold}\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Title/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\nimport { Textarea } from 'shared/components';\n\nexport const TitleTextarea = styled(Textarea)`\n  margin: 18px 0 0 -8px;\n  height: 44px;\n  width: 100%;\n  textarea {\n    padding: 7px 7px 8px;\n    line-height: 1.28;\n    border: none;\n    resize: none;\n    background: #fff;\n    border: 1px solid transparent;\n    box-shadow: 0 0 0 1px transparent;\n    transition: background 0.1s;\n    ${font.size(24)}\n    ${font.medium}\n    &:hover:not(:focus) {\n      background: ${color.backgroundLight};\n    }\n  }\n`;\n\nexport const ErrorText = styled.div`\n  padding-top: 4px;\n  color: ${color.danger};\n  ${font.size(13)}\n  ${font.medium}\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Title/index.jsx",
    "content": "import React, { Fragment, useRef, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { KeyCodes } from 'shared/constants/keyCodes';\nimport { is, generateErrors } from 'shared/utils/validation';\n\nimport { TitleTextarea, ErrorText } from './Styles';\n\nconst propTypes = {\n  issue: PropTypes.object.isRequired,\n  updateIssue: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsTitle = ({ issue, updateIssue }) => {\n  const $titleInputRef = useRef();\n  const [error, setError] = useState(null);\n\n  const handleTitleChange = () => {\n    setError(null);\n\n    const title = $titleInputRef.current.value;\n    if (title === issue.title) return;\n\n    const errors = generateErrors({ title }, { title: [is.required(), is.maxLength(200)] });\n\n    if (errors.title) {\n      setError(errors.title);\n    } else {\n      updateIssue({ title });\n    }\n  };\n\n  return (\n    <Fragment>\n      <TitleTextarea\n        minRows={1}\n        placeholder=\"Short summary\"\n        defaultValue={issue.title}\n        ref={$titleInputRef}\n        onBlur={handleTitleChange}\n        onKeyDown={event => {\n          if (event.keyCode === KeyCodes.ENTER) {\n            event.target.blur();\n          }\n        }}\n      />\n      {error && <ErrorText>{error}</ErrorText>}\n    </Fragment>\n  );\n};\n\nProjectBoardIssueDetailsTitle.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsTitle;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Type/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\nimport { Button } from 'shared/components';\n\nexport const TypeButton = styled(Button)`\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n  color: ${color.textMedium};\n  ${font.size(13)}\n`;\n\nexport const Type = styled.div`\n  display: flex;\n  align-items: center;\n`;\n\nexport const TypeLabel = styled.div`\n  padding: 0 5px 0 7px;\n  ${font.size(15)}\n`;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/Type/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { IssueType, IssueTypeCopy } from 'shared/constants/issues';\nimport { IssueTypeIcon, Select } from 'shared/components';\n\nimport { TypeButton, Type, TypeLabel } from './Styles';\n\nconst propTypes = {\n  issue: PropTypes.object.isRequired,\n  updateIssue: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (\n  <Select\n    variant=\"empty\"\n    dropdownWidth={150}\n    withClearValue={false}\n    name=\"type\"\n    value={issue.type}\n    options={Object.values(IssueType).map(type => ({\n      value: type,\n      label: IssueTypeCopy[type],\n    }))}\n    onChange={type => updateIssue({ type })}\n    renderValue={({ value: type }) => (\n      <TypeButton variant=\"empty\" icon={<IssueTypeIcon type={type} />}>\n        {`${IssueTypeCopy[type]}-${issue.id}`}\n      </TypeButton>\n    )}\n    renderOption={({ value: type }) => (\n      <Type key={type} onClick={() => updateIssue({ type })}>\n        <IssueTypeIcon type={type} top={1} />\n        <TypeLabel>{IssueTypeCopy[type]}</TypeLabel>\n      </Type>\n    )}\n  />\n);\n\nProjectBoardIssueDetailsType.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetailsType;\n"
  },
  {
    "path": "client/src/Project/Board/IssueDetails/index.jsx",
    "content": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\n\nimport api from 'shared/utils/api';\nimport useApi from 'shared/hooks/api';\nimport { PageError, CopyLinkButton, Button, AboutTooltip } from 'shared/components';\n\nimport Loader from './Loader';\nimport Type from './Type';\nimport Delete from './Delete';\nimport Title from './Title';\nimport Description from './Description';\nimport Comments from './Comments';\nimport Status from './Status';\nimport AssigneesReporter from './AssigneesReporter';\nimport Priority from './Priority';\nimport EstimateTracking from './EstimateTracking';\nimport Dates from './Dates';\nimport { TopActions, TopActionsRight, Content, Left, Right } from './Styles';\n\nconst propTypes = {\n  issueId: PropTypes.string.isRequired,\n  projectUsers: PropTypes.array.isRequired,\n  fetchProject: PropTypes.func.isRequired,\n  updateLocalProjectIssues: PropTypes.func.isRequired,\n  modalClose: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardIssueDetails = ({\n  issueId,\n  projectUsers,\n  fetchProject,\n  updateLocalProjectIssues,\n  modalClose,\n}) => {\n  const [{ data, error, setLocalData }, fetchIssue] = useApi.get(`/issues/${issueId}`);\n\n  if (!data) return <Loader />;\n  if (error) return <PageError />;\n\n  const { issue } = data;\n\n  const updateLocalIssueDetails = fields =>\n    setLocalData(currentData => ({ issue: { ...currentData.issue, ...fields } }));\n\n  const updateIssue = updatedFields => {\n    api.optimisticUpdate(`/issues/${issueId}`, {\n      updatedFields,\n      currentFields: issue,\n      setLocalData: fields => {\n        updateLocalIssueDetails(fields);\n        updateLocalProjectIssues(issue.id, fields);\n      },\n    });\n  };\n\n  return (\n    <Fragment>\n      <TopActions>\n        <Type issue={issue} updateIssue={updateIssue} />\n        <TopActionsRight>\n          <AboutTooltip\n            renderLink={linkProps => (\n              <Button icon=\"feedback\" variant=\"empty\" {...linkProps}>\n                Give feedback\n              </Button>\n            )}\n          />\n          <CopyLinkButton variant=\"empty\" />\n          <Delete issue={issue} fetchProject={fetchProject} modalClose={modalClose} />\n          <Button icon=\"close\" iconSize={24} variant=\"empty\" onClick={modalClose} />\n        </TopActionsRight>\n      </TopActions>\n      <Content>\n        <Left>\n          <Title issue={issue} updateIssue={updateIssue} />\n          <Description issue={issue} updateIssue={updateIssue} />\n          <Comments issue={issue} fetchIssue={fetchIssue} />\n        </Left>\n        <Right>\n          <Status issue={issue} updateIssue={updateIssue} />\n          <AssigneesReporter issue={issue} updateIssue={updateIssue} projectUsers={projectUsers} />\n          <Priority issue={issue} updateIssue={updateIssue} />\n          <EstimateTracking issue={issue} updateIssue={updateIssue} />\n          <Dates issue={issue} />\n        </Right>\n      </Content>\n    </Fragment>\n  );\n};\n\nProjectBoardIssueDetails.propTypes = propTypes;\n\nexport default ProjectBoardIssueDetails;\n"
  },
  {
    "path": "client/src/Project/Board/Lists/List/Issue/Styles.js",
    "content": "import styled, { css } from 'styled-components';\nimport { Link } from 'react-router-dom';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport { Avatar } from 'shared/components';\n\nexport const IssueLink = styled(Link)`\n  display: block;\n  margin-bottom: 5px;\n`;\n\nexport const Issue = styled.div`\n  padding: 10px;\n  border-radius: 3px;\n  background: #fff;\n  box-shadow: 0px 1px 2px 0px rgba(9, 30, 66, 0.25);\n  transition: background 0.1s;\n  ${mixin.clickable}\n  @media (max-width: 1100px) {\n    padding: 10px 8px;\n  }\n  &:hover {\n    background: ${color.backgroundLight};\n  }\n  ${props =>\n    props.isBeingDragged &&\n    css`\n      transform: rotate(3deg);\n      box-shadow: 5px 10px 30px 0px rgba(9, 30, 66, 0.15);\n    `}\n`;\n\nexport const Title = styled.p`\n  padding-bottom: 11px;\n  ${font.size(15)}\n  @media (max-width: 1100px) {\n    ${font.size(14.5)}\n  }\n`;\n\nexport const Bottom = styled.div`\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n`;\n\nexport const Assignees = styled.div`\n  display: flex;\n  flex-direction: row-reverse;\n  margin-left: 2px;\n`;\n\nexport const AssigneeAvatar = styled(Avatar)`\n  margin-left: -2px;\n  box-shadow: 0 0 0 2px #fff;\n`;\n"
  },
  {
    "path": "client/src/Project/Board/Lists/List/Issue/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { useRouteMatch } from 'react-router-dom';\nimport { Draggable } from 'react-beautiful-dnd';\n\nimport { IssueTypeIcon, IssuePriorityIcon } from 'shared/components';\n\nimport { IssueLink, Issue, Title, Bottom, Assignees, AssigneeAvatar } from './Styles';\n\nconst propTypes = {\n  projectUsers: PropTypes.array.isRequired,\n  issue: PropTypes.object.isRequired,\n  index: PropTypes.number.isRequired,\n};\n\nconst ProjectBoardListIssue = ({ projectUsers, issue, index }) => {\n  const match = useRouteMatch();\n\n  const assignees = issue.userIds.map(userId => projectUsers.find(user => user.id === userId));\n\n  return (\n    <Draggable draggableId={issue.id.toString()} index={index}>\n      {(provided, snapshot) => (\n        <IssueLink\n          to={`${match.url}/issues/${issue.id}`}\n          ref={provided.innerRef}\n          data-testid=\"list-issue\"\n          {...provided.draggableProps}\n          {...provided.dragHandleProps}\n        >\n          <Issue isBeingDragged={snapshot.isDragging && !snapshot.isDropAnimating}>\n            <Title>{issue.title}</Title>\n            <Bottom>\n              <div>\n                <IssueTypeIcon type={issue.type} />\n                <IssuePriorityIcon priority={issue.priority} top={-1} left={4} />\n              </div>\n              <Assignees>\n                {assignees.map(user => (\n                  <AssigneeAvatar\n                    key={user.id}\n                    size={24}\n                    avatarUrl={user.avatarUrl}\n                    name={user.name}\n                  />\n                ))}\n              </Assignees>\n            </Bottom>\n          </Issue>\n        </IssueLink>\n      )}\n    </Draggable>\n  );\n};\n\nProjectBoardListIssue.propTypes = propTypes;\n\nexport default ProjectBoardListIssue;\n"
  },
  {
    "path": "client/src/Project/Board/Lists/List/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\n\nexport const List = styled.div`\n  display: flex;\n  flex-direction: column;\n  margin: 0 5px;\n  min-height: 400px;\n  width: 25%;\n  border-radius: 3px;\n  background: ${color.backgroundLightest};\n`;\n\nexport const Title = styled.div`\n  padding: 13px 10px 17px;\n  text-transform: uppercase;\n  color: ${color.textMedium};\n  ${font.size(12.5)};\n  ${mixin.truncateText}\n`;\n\nexport const IssuesCount = styled.span`\n  text-transform: lowercase;\n  ${font.size(13)};\n`;\n\nexport const Issues = styled.div`\n  height: 100%;\n  padding: 0 5px;\n`;\n"
  },
  {
    "path": "client/src/Project/Board/Lists/List/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport moment from 'moment';\nimport { Droppable } from 'react-beautiful-dnd';\nimport { intersection } from 'lodash';\n\nimport { IssueStatusCopy } from 'shared/constants/issues';\n\nimport Issue from './Issue';\nimport { List, Title, IssuesCount, Issues } from './Styles';\n\nconst propTypes = {\n  status: PropTypes.string.isRequired,\n  project: PropTypes.object.isRequired,\n  filters: PropTypes.object.isRequired,\n  currentUserId: PropTypes.number,\n};\n\nconst defaultProps = {\n  currentUserId: null,\n};\n\nconst ProjectBoardList = ({ status, project, filters, currentUserId }) => {\n  const filteredIssues = filterIssues(project.issues, filters, currentUserId);\n  const filteredListIssues = getSortedListIssues(filteredIssues, status);\n  const allListIssues = getSortedListIssues(project.issues, status);\n\n  return (\n    <Droppable key={status} droppableId={status}>\n      {provided => (\n        <List>\n          <Title>\n            {`${IssueStatusCopy[status]} `}\n            <IssuesCount>{formatIssuesCount(allListIssues, filteredListIssues)}</IssuesCount>\n          </Title>\n          <Issues\n            {...provided.droppableProps}\n            ref={provided.innerRef}\n            data-testid={`board-list:${status}`}\n          >\n            {filteredListIssues.map((issue, index) => (\n              <Issue key={issue.id} projectUsers={project.users} issue={issue} index={index} />\n            ))}\n            {provided.placeholder}\n          </Issues>\n        </List>\n      )}\n    </Droppable>\n  );\n};\n\nconst filterIssues = (projectIssues, filters, currentUserId) => {\n  const { searchTerm, userIds, myOnly, recent } = filters;\n  let issues = projectIssues;\n\n  if (searchTerm) {\n    issues = issues.filter(issue => issue.title.toLowerCase().includes(searchTerm.toLowerCase()));\n  }\n  if (userIds.length > 0) {\n    issues = issues.filter(issue => intersection(issue.userIds, userIds).length > 0);\n  }\n  if (myOnly && currentUserId) {\n    issues = issues.filter(issue => issue.userIds.includes(currentUserId));\n  }\n  if (recent) {\n    issues = issues.filter(issue => moment(issue.updatedAt).isAfter(moment().subtract(3, 'days')));\n  }\n  return issues;\n};\n\nconst getSortedListIssues = (issues, status) =>\n  issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition);\n\nconst formatIssuesCount = (allListIssues, filteredListIssues) => {\n  if (allListIssues.length !== filteredListIssues.length) {\n    return `${filteredListIssues.length} of ${allListIssues.length}`;\n  }\n  return allListIssues.length;\n};\n\nProjectBoardList.propTypes = propTypes;\nProjectBoardList.defaultProps = defaultProps;\n\nexport default ProjectBoardList;\n"
  },
  {
    "path": "client/src/Project/Board/Lists/Styles.js",
    "content": "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",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { DragDropContext } from 'react-beautiful-dnd';\n\nimport useCurrentUser from 'shared/hooks/currentUser';\nimport api from 'shared/utils/api';\nimport { moveItemWithinArray, insertItemIntoArray } from 'shared/utils/javascript';\nimport { IssueStatus } from 'shared/constants/issues';\n\nimport List from './List';\nimport { Lists } from './Styles';\n\nconst propTypes = {\n  project: PropTypes.object.isRequired,\n  filters: PropTypes.object.isRequired,\n  updateLocalProjectIssues: PropTypes.func.isRequired,\n};\n\nconst ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => {\n  const { currentUserId } = useCurrentUser();\n\n  const handleIssueDrop = ({ draggableId, destination, source }) => {\n    if (!isPositionChanged(source, destination)) return;\n\n    const issueId = Number(draggableId);\n\n    api.optimisticUpdate(`/issues/${issueId}`, {\n      updatedFields: {\n        status: destination.droppableId,\n        listPosition: calculateIssueListPosition(project.issues, destination, source, issueId),\n      },\n      currentFields: project.issues.find(({ id }) => id === issueId),\n      setLocalData: fields => updateLocalProjectIssues(issueId, fields),\n    });\n  };\n\n  return (\n    <DragDropContext onDragEnd={handleIssueDrop}>\n      <Lists>\n        {Object.values(IssueStatus).map(status => (\n          <List\n            key={status}\n            status={status}\n            project={project}\n            filters={filters}\n            currentUserId={currentUserId}\n          />\n        ))}\n      </Lists>\n    </DragDropContext>\n  );\n};\n\nconst isPositionChanged = (destination, source) => {\n  if (!destination) return false;\n  const isSameList = destination.droppableId === source.droppableId;\n  const isSamePosition = destination.index === source.index;\n  return !isSameList || !isSamePosition;\n};\n\nconst calculateIssueListPosition = (...args) => {\n  const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(...args);\n  let position;\n\n  if (!prevIssue && !nextIssue) {\n    position = 1;\n  } else if (!prevIssue) {\n    position = nextIssue.listPosition - 1;\n  } else if (!nextIssue) {\n    position = prevIssue.listPosition + 1;\n  } else {\n    position = prevIssue.listPosition + (nextIssue.listPosition - prevIssue.listPosition) / 2;\n  }\n  return position;\n};\n\nconst getAfterDropPrevNextIssue = (allIssues, destination, source, droppedIssueId) => {\n  const beforeDropDestinationIssues = getSortedListIssues(allIssues, destination.droppableId);\n  const droppedIssue = allIssues.find(issue => issue.id === droppedIssueId);\n  const isSameList = destination.droppableId === source.droppableId;\n\n  const afterDropDestinationIssues = isSameList\n    ? moveItemWithinArray(beforeDropDestinationIssues, droppedIssue, destination.index)\n    : insertItemIntoArray(beforeDropDestinationIssues, droppedIssue, destination.index);\n\n  return {\n    prevIssue: afterDropDestinationIssues[destination.index - 1],\n    nextIssue: afterDropDestinationIssues[destination.index + 1],\n  };\n};\n\nconst getSortedListIssues = (issues, status) =>\n  issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition);\n\nProjectBoardLists.propTypes = propTypes;\n\nexport default ProjectBoardLists;\n"
  },
  {
    "path": "client/src/Project/Board/index.jsx",
    "content": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\nimport { Route, useRouteMatch, useHistory } from 'react-router-dom';\n\nimport useMergeState from 'shared/hooks/mergeState';\nimport { Breadcrumbs, Modal } from 'shared/components';\n\nimport Header from './Header';\nimport Filters from './Filters';\nimport Lists from './Lists';\nimport IssueDetails from './IssueDetails';\n\nconst propTypes = {\n  project: PropTypes.object.isRequired,\n  fetchProject: PropTypes.func.isRequired,\n  updateLocalProjectIssues: PropTypes.func.isRequired,\n};\n\nconst defaultFilters = {\n  searchTerm: '',\n  userIds: [],\n  myOnly: false,\n  recent: false,\n};\n\nconst ProjectBoard = ({ project, fetchProject, updateLocalProjectIssues }) => {\n  const match = useRouteMatch();\n  const history = useHistory();\n\n  const [filters, mergeFilters] = useMergeState(defaultFilters);\n\n  return (\n    <Fragment>\n      <Breadcrumbs items={['Projects', project.name, 'Kanban Board']} />\n      <Header />\n      <Filters\n        projectUsers={project.users}\n        defaultFilters={defaultFilters}\n        filters={filters}\n        mergeFilters={mergeFilters}\n      />\n      <Lists\n        project={project}\n        filters={filters}\n        updateLocalProjectIssues={updateLocalProjectIssues}\n      />\n      <Route\n        path={`${match.path}/issues/:issueId`}\n        render={routeProps => (\n          <Modal\n            isOpen\n            testid=\"modal:issue-details\"\n            width={1040}\n            withCloseIcon={false}\n            onClose={() => history.push(match.url)}\n            renderContent={modal => (\n              <IssueDetails\n                issueId={routeProps.match.params.issueId}\n                projectUsers={project.users}\n                fetchProject={fetchProject}\n                updateLocalProjectIssues={updateLocalProjectIssues}\n                modalClose={modal.close}\n              />\n            )}\n          />\n        )}\n      />\n    </Fragment>\n  );\n};\n\nProjectBoard.propTypes = propTypes;\n\nexport default ProjectBoard;\n"
  },
  {
    "path": "client/src/Project/IssueCreate/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\nimport { Button, Form } from 'shared/components';\n\nexport const FormElement = styled(Form.Element)`\n  padding: 25px 40px 35px;\n`;\n\nexport const FormHeading = styled.div`\n  padding-bottom: 15px;\n  ${font.size(21)}\n`;\n\nexport const SelectItem = styled.div`\n  display: flex;\n  align-items: center;\n  margin-right: 15px;\n  ${props => props.withBottomMargin && `margin-bottom: 5px;`}\n`;\n\nexport const SelectItemLabel = styled.div`\n  padding: 0 3px 0 6px;\n`;\n\nexport const Divider = styled.div`\n  margin-top: 22px;\n  border-top: 1px solid ${color.borderLightest};\n`;\n\nexport const Actions = styled.div`\n  display: flex;\n  justify-content: flex-end;\n  padding-top: 30px;\n`;\n\nexport const ActionButton = styled(Button)`\n  margin-left: 10px;\n`;\n"
  },
  {
    "path": "client/src/Project/IssueCreate/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport {\n  IssueType,\n  IssueStatus,\n  IssuePriority,\n  IssueTypeCopy,\n  IssuePriorityCopy,\n} from 'shared/constants/issues';\nimport toast from 'shared/utils/toast';\nimport useApi from 'shared/hooks/api';\nimport useCurrentUser from 'shared/hooks/currentUser';\nimport { Form, IssueTypeIcon, Icon, Avatar, IssuePriorityIcon } from 'shared/components';\n\nimport {\n  FormHeading,\n  FormElement,\n  SelectItem,\n  SelectItemLabel,\n  Divider,\n  Actions,\n  ActionButton,\n} from './Styles';\n\nconst propTypes = {\n  project: PropTypes.object.isRequired,\n  fetchProject: PropTypes.func.isRequired,\n  onCreate: PropTypes.func.isRequired,\n  modalClose: PropTypes.func.isRequired,\n};\n\nconst ProjectIssueCreate = ({ project, fetchProject, onCreate, modalClose }) => {\n  const [{ isCreating }, createIssue] = useApi.post('/issues');\n\n  const { currentUserId } = useCurrentUser();\n\n  return (\n    <Form\n      enableReinitialize\n      initialValues={{\n        type: IssueType.TASK,\n        title: '',\n        description: '',\n        reporterId: currentUserId,\n        userIds: [],\n        priority: IssuePriority.MEDIUM,\n      }}\n      validations={{\n        type: Form.is.required(),\n        title: [Form.is.required(), Form.is.maxLength(200)],\n        reporterId: Form.is.required(),\n        priority: Form.is.required(),\n      }}\n      onSubmit={async (values, form) => {\n        try {\n          await createIssue({\n            ...values,\n            status: IssueStatus.BACKLOG,\n            projectId: project.id,\n            users: values.userIds.map(id => ({ id })),\n          });\n          await fetchProject();\n          toast.success('Issue has been successfully created.');\n          onCreate();\n        } catch (error) {\n          Form.handleAPIError(error, form);\n        }\n      }}\n    >\n      <FormElement>\n        <FormHeading>Create issue</FormHeading>\n        <Form.Field.Select\n          name=\"type\"\n          label=\"Issue Type\"\n          tip=\"Start typing to get a list of possible matches.\"\n          options={typeOptions}\n          renderOption={renderType}\n          renderValue={renderType}\n        />\n        <Divider />\n        <Form.Field.Input\n          name=\"title\"\n          label=\"Short Summary\"\n          tip=\"Concisely summarize the issue in one or two sentences.\"\n        />\n        <Form.Field.TextEditor\n          name=\"description\"\n          label=\"Description\"\n          tip=\"Describe the issue in as much detail as you'd like.\"\n        />\n        <Form.Field.Select\n          name=\"reporterId\"\n          label=\"Reporter\"\n          options={userOptions(project)}\n          renderOption={renderUser(project)}\n          renderValue={renderUser(project)}\n        />\n        <Form.Field.Select\n          isMulti\n          name=\"userIds\"\n          label=\"Assignees\"\n          tio=\"People who are responsible for dealing with this issue.\"\n          options={userOptions(project)}\n          renderOption={renderUser(project)}\n          renderValue={renderUser(project)}\n        />\n        <Form.Field.Select\n          name=\"priority\"\n          label=\"Priority\"\n          tip=\"Priority in relation to other issues.\"\n          options={priorityOptions}\n          renderOption={renderPriority}\n          renderValue={renderPriority}\n        />\n        <Actions>\n          <ActionButton type=\"submit\" variant=\"primary\" isWorking={isCreating}>\n            Create Issue\n          </ActionButton>\n          <ActionButton type=\"button\" variant=\"empty\" onClick={modalClose}>\n            Cancel\n          </ActionButton>\n        </Actions>\n      </FormElement>\n    </Form>\n  );\n};\n\nconst typeOptions = Object.values(IssueType).map(type => ({\n  value: type,\n  label: IssueTypeCopy[type],\n}));\n\nconst priorityOptions = Object.values(IssuePriority).map(priority => ({\n  value: priority,\n  label: IssuePriorityCopy[priority],\n}));\n\nconst userOptions = project => project.users.map(user => ({ value: user.id, label: user.name }));\n\nconst renderType = ({ value: type }) => (\n  <SelectItem>\n    <IssueTypeIcon type={type} top={1} />\n    <SelectItemLabel>{IssueTypeCopy[type]}</SelectItemLabel>\n  </SelectItem>\n);\n\nconst renderPriority = ({ value: priority }) => (\n  <SelectItem>\n    <IssuePriorityIcon priority={priority} top={1} />\n    <SelectItemLabel>{IssuePriorityCopy[priority]}</SelectItemLabel>\n  </SelectItem>\n);\n\nconst renderUser = project => ({ value: userId, removeOptionValue }) => {\n  const user = project.users.find(({ id }) => id === userId);\n\n  return (\n    <SelectItem\n      key={user.id}\n      withBottomMargin={!!removeOptionValue}\n      onClick={() => removeOptionValue && removeOptionValue()}\n    >\n      <Avatar size={20} avatarUrl={user.avatarUrl} name={user.name} />\n      <SelectItemLabel>{user.name}</SelectItemLabel>\n      {removeOptionValue && <Icon type=\"close\" top={2} />}\n    </SelectItem>\n  );\n};\n\nProjectIssueCreate.propTypes = propTypes;\n\nexport default ProjectIssueCreate;\n"
  },
  {
    "path": "client/src/Project/IssueSearch/NoResultsSvg.jsx",
    "content": "import React from 'react';\n\nconst NoResults = () => (\n  <svg\n    width=\"160px\"\n    height=\"146px\"\n    viewBox=\"0 0 160 146\"\n    version=\"1.1\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <defs>\n      <linearGradient\n        x1=\"14.2197515%\"\n        y1=\"85.3653884%\"\n        x2=\"85.2573455%\"\n        y2=\"14.6507444%\"\n        id=\"linearGradient-1\"\n      >\n        <stop stopColor=\"#C1C7D0\" offset=\"56%\" />\n        <stop stopColor=\"#E9EBEF\" stopOpacity=\"0.5\" offset=\"97%\" />\n      </linearGradient>\n    </defs>\n    <g stroke=\"none\" strokeWidth=\"1\" fill=\"none\" fillRule=\"evenodd\">\n      <g transform=\"translate(-300.000000, -352.000000)\" fillRule=\"nonzero\">\n        <g id=\"No-Results\" transform=\"translate(300.000000, 352.000000)\">\n          <g id=\"Group\" opacity=\"0.3\" transform=\"translate(8.421053, 5.350785)\" fill=\"#B3BAC5\">\n            <path\n              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\"\n              id=\"Shape\"\n            />\n          </g>\n          <g id=\"Group\" opacity=\"0.3\" transform=\"translate(143.157895, 9.937173)\" fill=\"#C1C7D0\">\n            <path\n              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\"\n              id=\"Shape\"\n            />\n          </g>\n          <g id=\"Group\" opacity=\"0.3\" transform=\"translate(11.483254, 0.000000)\" fill=\"#C1C7D0\">\n            <path\n              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\"\n              id=\"Shape\"\n            />\n          </g>\n          <g id=\"Group\" opacity=\"0.3\" transform=\"translate(0.000000, 120.774869)\" fill=\"#C1C7D0\">\n            <path\n              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\"\n              id=\"Shape\"\n            />\n          </g>\n          <path\n            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\"\n            id=\"Shape\"\n            fill=\"#CFD4DB\"\n          />\n          <path\n            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\"\n            id=\"Shape\"\n            fill=\"#DFE1E5\"\n            style={{ mixBlendMode: 'multiply' }}\n          />\n          <path\n            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\"\n            id=\"_Compound_Clipping_Path_\"\n            fill=\"url(#linearGradient-1)\"\n          />\n          <path\n            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\"\n            id=\"_Clipping_Path_\"\n            fill=\"#C1C7D0\"\n          />\n        </g>\n      </g>\n    </g>\n  </svg>\n);\n\nexport default NoResults;\n"
  },
  {
    "path": "client/src/Project/IssueSearch/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport { InputDebounced, Spinner, Icon } from 'shared/components';\n\nexport const IssueSearch = styled.div`\n  padding: 25px 35px 60px;\n`;\n\nexport const SearchInputCont = styled.div`\n  position: relative;\n  padding-right: 30px;\n  margin-bottom: 40px;\n`;\n\nexport const SearchInputDebounced = styled(InputDebounced)`\n  height: 40px;\n  input {\n    padding: 0 0 0 32px;\n    border: none;\n    border-bottom: 2px solid ${color.primary};\n    background: #fff;\n    ${font.size(21)}\n    &:focus,\n    &:hover {\n      box-shadow: none;\n      border: none;\n      border-bottom: 2px solid ${color.primary};\n      background: #fff;\n    }\n  }\n`;\n\nexport const SearchIcon = styled(Icon)`\n  position: absolute;\n  top: 8px;\n  left: 0;\n  color: ${color.textMedium};\n`;\n\nexport const SearchSpinner = styled(Spinner)`\n  position: absolute;\n  top: 5px;\n  right: 30px;\n`;\n\nexport const Issue = styled.div`\n  display: flex;\n  align-items: center;\n  padding: 4px 10px;\n  border-radius: 4px;\n  transition: background 0.1s;\n  ${mixin.clickable}\n  &:hover {\n    background: ${color.backgroundLight};\n  }\n`;\n\nexport const IssueData = styled.div`\n  padding-left: 15px;\n`;\n\nexport const IssueTitle = styled.div`\n  color: ${color.textDark};\n  ${font.size(15)}\n`;\n\nexport const IssueTypeId = styled.div`\n  text-transform: uppercase;\n  color: ${color.textMedium};\n  ${font.size(12.5)}\n`;\n\nexport const SectionTitle = styled.div`\n  padding-bottom: 12px;\n  text-transform: uppercase;\n  color: ${color.textMedium};\n  ${font.bold}\n  ${font.size(11.5)}\n`;\n\nexport const NoResults = styled.div`\n  padding-top: 50px;\n  text-align: center;\n`;\n\nexport const NoResultsTitle = styled.div`\n  padding-top: 30px;\n  ${font.medium}\n  ${font.size(20)}\n`;\n\nexport const NoResultsTip = styled.div`\n  padding-top: 10px;\n  ${font.size(15)}\n`;\n"
  },
  {
    "path": "client/src/Project/IssueSearch/index.jsx",
    "content": "import React, { Fragment, useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { Link } from 'react-router-dom';\nimport { get } from 'lodash';\n\nimport useApi from 'shared/hooks/api';\nimport { sortByNewest } from 'shared/utils/javascript';\nimport { IssueTypeIcon } from 'shared/components';\n\nimport NoResultsSVG from './NoResultsSvg';\nimport {\n  IssueSearch,\n  SearchInputCont,\n  SearchInputDebounced,\n  SearchIcon,\n  SearchSpinner,\n  Issue,\n  IssueData,\n  IssueTitle,\n  IssueTypeId,\n  SectionTitle,\n  NoResults,\n  NoResultsTitle,\n  NoResultsTip,\n} from './Styles';\n\nconst propTypes = {\n  project: PropTypes.object.isRequired,\n};\n\nconst ProjectIssueSearch = ({ project }) => {\n  const [isSearchTermEmpty, setIsSearchTermEmpty] = useState(true);\n\n  const [{ data, isLoading }, fetchIssues] = useApi.get('/issues', {}, { lazy: true });\n\n  const matchingIssues = get(data, 'issues', []);\n\n  const recentIssues = sortByNewest(project.issues, 'createdAt').slice(0, 10);\n\n  const handleSearchChange = value => {\n    const searchTerm = value.trim();\n\n    setIsSearchTermEmpty(!searchTerm);\n\n    if (searchTerm) {\n      fetchIssues({ searchTerm });\n    }\n  };\n\n  return (\n    <IssueSearch>\n      <SearchInputCont>\n        <SearchInputDebounced\n          autoFocus\n          placeholder=\"Search issues by summary, description...\"\n          onChange={handleSearchChange}\n        />\n        <SearchIcon type=\"search\" size={22} />\n        {isLoading && <SearchSpinner />}\n      </SearchInputCont>\n\n      {isSearchTermEmpty && recentIssues.length > 0 && (\n        <Fragment>\n          <SectionTitle>Recent Issues</SectionTitle>\n          {recentIssues.map(renderIssue)}\n        </Fragment>\n      )}\n\n      {!isSearchTermEmpty && matchingIssues.length > 0 && (\n        <Fragment>\n          <SectionTitle>Matching Issues</SectionTitle>\n          {matchingIssues.map(renderIssue)}\n        </Fragment>\n      )}\n\n      {!isSearchTermEmpty && !isLoading && matchingIssues.length === 0 && (\n        <NoResults>\n          <NoResultsSVG />\n          <NoResultsTitle>We couldn&apos;t find anything matching your search</NoResultsTitle>\n          <NoResultsTip>Try again with a different term.</NoResultsTip>\n        </NoResults>\n      )}\n    </IssueSearch>\n  );\n};\n\nconst renderIssue = issue => (\n  <Link key={issue.id} to={`/project/board/issues/${issue.id}`}>\n    <Issue>\n      <IssueTypeIcon type={issue.type} size={25} />\n      <IssueData>\n        <IssueTitle>{issue.title}</IssueTitle>\n        <IssueTypeId>{`${issue.type}-${issue.id}`}</IssueTypeId>\n      </IssueData>\n    </Issue>\n  </Link>\n);\n\nProjectIssueSearch.propTypes = propTypes;\n\nexport default ProjectIssueSearch;\n"
  },
  {
    "path": "client/src/Project/NavbarLeft/Styles.js",
    "content": "import styled from 'styled-components';\nimport { NavLink } from 'react-router-dom';\n\nimport { font, sizes, color, mixin, zIndexValues } from 'shared/utils/styles';\nimport { Logo } from 'shared/components';\n\nexport const NavLeft = styled.aside`\n  z-index: ${zIndexValues.navLeft};\n  position: fixed;\n  top: 0;\n  left: 0;\n  overflow-x: hidden;\n  height: 100vh;\n  width: ${sizes.appNavBarLeftWidth}px;\n  background: ${color.backgroundDarkPrimary};\n  transition: all 0.1s;\n  ${mixin.hardwareAccelerate}\n  &:hover {\n    width: 200px;\n    box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.6);\n  }\n`;\n\nexport const LogoLink = styled(NavLink)`\n  display: block;\n  position: relative;\n  left: 0;\n  margin: 20px 0 10px;\n  transition: left 0.1s;\n`;\n\nexport const StyledLogo = styled(Logo)`\n  display: inline-block;\n  margin-left: 8px;\n  padding: 10px;\n  ${mixin.clickable}\n`;\n\nexport const Bottom = styled.div`\n  position: absolute;\n  bottom: 20px;\n  left: 0;\n  width: 100%;\n`;\n\nexport const Item = styled.div`\n  position: relative;\n  width: 100%;\n  height: 42px;\n  line-height: 42px;\n  padding-left: 64px;\n  color: #deebff;\n  transition: color 0.1s;\n  ${mixin.clickable}\n  &:hover {\n    background: rgba(255, 255, 255, 0.1);\n  }\n  i {\n    position: absolute;\n    left: 18px;\n  }\n`;\n\nexport const ItemText = styled.div`\n  position: relative;\n  right: 12px;\n  visibility: hidden;\n  opacity: 0;\n  text-transform: uppercase;\n  transition: all 0.1s;\n  transition-property: right, visibility, opacity;\n  ${font.bold}\n  ${font.size(12)}\n  ${NavLeft}:hover & {\n    right: 0;\n    visibility: visible;\n    opacity: 1;\n  }\n`;\n"
  },
  {
    "path": "client/src/Project/NavbarLeft/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Icon, AboutTooltip } from 'shared/components';\n\nimport { NavLeft, LogoLink, StyledLogo, Bottom, Item, ItemText } from './Styles';\n\nconst propTypes = {\n  issueSearchModalOpen: PropTypes.func.isRequired,\n  issueCreateModalOpen: PropTypes.func.isRequired,\n};\n\nconst ProjectNavbarLeft = ({ issueSearchModalOpen, issueCreateModalOpen }) => (\n  <NavLeft>\n    <LogoLink to=\"/\">\n      <StyledLogo color=\"#fff\" />\n    </LogoLink>\n\n    <Item onClick={issueSearchModalOpen}>\n      <Icon type=\"search\" size={22} top={1} left={3} />\n      <ItemText>Search issues</ItemText>\n    </Item>\n\n    <Item onClick={issueCreateModalOpen}>\n      <Icon type=\"plus\" size={27} />\n      <ItemText>Create Issue</ItemText>\n    </Item>\n\n    <Bottom>\n      <AboutTooltip\n        placement=\"right\"\n        offset={{ top: -218 }}\n        renderLink={linkProps => (\n          <Item {...linkProps}>\n            <Icon type=\"help\" size={25} />\n            <ItemText>About</ItemText>\n          </Item>\n        )}\n      />\n    </Bottom>\n  </NavLeft>\n);\n\nProjectNavbarLeft.propTypes = propTypes;\n\nexport default ProjectNavbarLeft;\n"
  },
  {
    "path": "client/src/Project/ProjectSettings/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\nimport { Button, Form } from 'shared/components';\n\nexport const FormCont = styled.div`\n  display: flex;\n  justify-content: center;\n`;\n\nexport const FormElement = styled(Form.Element)`\n  width: 100%;\n  max-width: 640px;\n`;\n\nexport const FormHeading = styled.h1`\n  padding: 6px 0 15px;\n  ${font.size(24)}\n  ${font.medium}\n`;\n\nexport const ActionButton = styled(Button)`\n  margin-top: 30px;\n`;\n"
  },
  {
    "path": "client/src/Project/ProjectSettings/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { ProjectCategory, ProjectCategoryCopy } from 'shared/constants/projects';\nimport toast from 'shared/utils/toast';\nimport useApi from 'shared/hooks/api';\nimport { Form, Breadcrumbs } from 'shared/components';\n\nimport { FormCont, FormHeading, FormElement, ActionButton } from './Styles';\n\nconst propTypes = {\n  project: PropTypes.object.isRequired,\n  fetchProject: PropTypes.func.isRequired,\n};\n\nconst ProjectSettings = ({ project, fetchProject }) => {\n  const [{ isUpdating }, updateProject] = useApi.put('/project');\n\n  return (\n    <Form\n      initialValues={Form.initialValues(project, get => ({\n        name: get('name'),\n        url: get('url'),\n        category: get('category'),\n        description: get('description'),\n      }))}\n      validations={{\n        name: [Form.is.required(), Form.is.maxLength(100)],\n        url: Form.is.url(),\n        category: Form.is.required(),\n      }}\n      onSubmit={async (values, form) => {\n        try {\n          await updateProject(values);\n          await fetchProject();\n          toast.success('Changes have been saved successfully.');\n        } catch (error) {\n          Form.handleAPIError(error, form);\n        }\n      }}\n    >\n      <FormCont>\n        <FormElement>\n          <Breadcrumbs items={['Projects', project.name, 'Project Details']} />\n          <FormHeading>Project Details</FormHeading>\n\n          <Form.Field.Input name=\"name\" label=\"Name\" />\n          <Form.Field.Input name=\"url\" label=\"URL\" />\n          <Form.Field.TextEditor\n            name=\"description\"\n            label=\"Description\"\n            tip=\"Describe the project in as much detail as you'd like.\"\n          />\n          <Form.Field.Select name=\"category\" label=\"Project Category\" options={categoryOptions} />\n\n          <ActionButton type=\"submit\" variant=\"primary\" isWorking={isUpdating}>\n            Save changes\n          </ActionButton>\n        </FormElement>\n      </FormCont>\n    </Form>\n  );\n};\n\nconst categoryOptions = Object.values(ProjectCategory).map(category => ({\n  value: category,\n  label: ProjectCategoryCopy[category],\n}));\n\nProjectSettings.propTypes = propTypes;\n\nexport default ProjectSettings;\n"
  },
  {
    "path": "client/src/Project/Sidebar/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, sizes, font, mixin, zIndexValues } from 'shared/utils/styles';\n\nexport const Sidebar = styled.div`\n  position: fixed;\n  z-index: ${zIndexValues.navLeft - 1};\n  top: 0;\n  left: ${sizes.appNavBarLeftWidth}px;\n  height: 100vh;\n  width: ${sizes.secondarySideBarWidth}px;\n  padding: 0 16px 24px;\n  background: ${color.backgroundLightest};\n  border-right: 1px solid ${color.borderLightest};\n  ${mixin.scrollableY}\n  ${mixin.customScrollbar()}\n  @media (max-width: 1100px) {\n    width: ${sizes.secondarySideBarWidth - 10}px;\n  }\n  @media (max-width: 999px) {\n    display: none;\n  }\n`;\n\nexport const ProjectInfo = styled.div`\n  display: flex;\n  padding: 24px 4px;\n`;\n\nexport const ProjectTexts = styled.div`\n  padding: 3px 0 0 10px;\n`;\n\nexport const ProjectName = styled.div`\n  color: ${color.textDark};\n  ${font.size(15)};\n  ${font.medium};\n`;\n\nexport const ProjectCategory = styled.div`\n  color: ${color.textMedium};\n  ${font.size(13)};\n`;\n\nexport const Divider = styled.div`\n  margin-top: 17px;\n  padding-top: 18px;\n  border-top: 1px solid ${color.borderLight};\n`;\n\nexport const LinkItem = styled.div`\n  position: relative;\n  display: flex;\n  padding: 8px 12px;\n  border-radius: 3px;\n  ${mixin.clickable}\n  ${props =>\n    !props.to ? `cursor: not-allowed;` : `&:hover { background: ${color.backgroundLight}; }`}\n  i {\n    margin-right: 15px;\n    font-size: 20px;\n  }\n  &.active {\n    color: ${color.primary};\n    background: ${color.backgroundLight};\n    i {\n      color: ${color.primary};\n    }\n  }\n`;\n\nexport const LinkText = styled.div`\n  padding-top: 2px;\n  ${font.size(14.7)};\n`;\n\nexport const NotImplemented = styled.div`\n  display: inline-block;\n  position: absolute;\n  top: 7px;\n  left: 40px;\n  width: 140px;\n  padding: 5px 0 5px 8px;\n  border-radius: 3px;\n  text-transform: uppercase;\n  color: ${color.textDark};\n  background: ${color.backgroundMedium};\n  opacity: 0;\n  ${font.size(11.5)};\n  ${font.bold}\n  ${LinkItem}:hover & {\n    opacity: 1;\n  }\n`;\n"
  },
  {
    "path": "client/src/Project/Sidebar/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { NavLink, useRouteMatch } from 'react-router-dom';\n\nimport { ProjectCategoryCopy } from 'shared/constants/projects';\nimport { Icon, ProjectAvatar } from 'shared/components';\n\nimport {\n  Sidebar,\n  ProjectInfo,\n  ProjectTexts,\n  ProjectName,\n  ProjectCategory,\n  Divider,\n  LinkItem,\n  LinkText,\n  NotImplemented,\n} from './Styles';\n\nconst propTypes = {\n  project: PropTypes.object.isRequired,\n};\n\nconst ProjectSidebar = ({ project }) => {\n  const match = useRouteMatch();\n\n  return (\n    <Sidebar>\n      <ProjectInfo>\n        <ProjectAvatar />\n        <ProjectTexts>\n          <ProjectName>{project.name}</ProjectName>\n          <ProjectCategory>{ProjectCategoryCopy[project.category]} project</ProjectCategory>\n        </ProjectTexts>\n      </ProjectInfo>\n\n      {renderLinkItem(match, 'Kanban Board', 'board', '/board')}\n      {renderLinkItem(match, 'Project settings', 'settings', '/settings')}\n      <Divider />\n      {renderLinkItem(match, 'Releases', 'shipping')}\n      {renderLinkItem(match, 'Issues and filters', 'issues')}\n      {renderLinkItem(match, 'Pages', 'page')}\n      {renderLinkItem(match, 'Reports', 'reports')}\n      {renderLinkItem(match, 'Components', 'component')}\n    </Sidebar>\n  );\n};\n\nconst renderLinkItem = (match, text, iconType, path) => {\n  const isImplemented = !!path;\n\n  const linkItemProps = isImplemented\n    ? { as: NavLink, exact: true, to: `${match.path}${path}` }\n    : { as: 'div' };\n\n  return (\n    <LinkItem {...linkItemProps}>\n      <Icon type={iconType} />\n      <LinkText>{text}</LinkText>\n      {!isImplemented && <NotImplemented>Not implemented</NotImplemented>}\n    </LinkItem>\n  );\n};\n\nProjectSidebar.propTypes = propTypes;\n\nexport default ProjectSidebar;\n"
  },
  {
    "path": "client/src/Project/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { sizes } from 'shared/utils/styles';\n\nconst paddingLeft = sizes.appNavBarLeftWidth + sizes.secondarySideBarWidth + 40;\n\nexport const ProjectPage = styled.div`\n  padding: 25px 32px 50px ${paddingLeft}px;\n\n  @media (max-width: 1100px) {\n    padding: 25px 20px 50px ${paddingLeft - 20}px;\n  }\n  @media (max-width: 999px) {\n    padding-left: ${paddingLeft - 20 - sizes.secondarySideBarWidth}px;\n  }\n`;\n"
  },
  {
    "path": "client/src/Project/index.jsx",
    "content": "import React from 'react';\nimport { Route, Redirect, useRouteMatch, useHistory } from 'react-router-dom';\n\nimport useApi from 'shared/hooks/api';\nimport { updateArrayItemById } from 'shared/utils/javascript';\nimport { createQueryParamModalHelpers } from 'shared/utils/queryParamModal';\nimport { PageLoader, PageError, Modal } from 'shared/components';\n\nimport NavbarLeft from './NavbarLeft';\nimport Sidebar from './Sidebar';\nimport Board from './Board';\nimport IssueSearch from './IssueSearch';\nimport IssueCreate from './IssueCreate';\nimport ProjectSettings from './ProjectSettings';\nimport { ProjectPage } from './Styles';\n\nconst Project = () => {\n  const match = useRouteMatch();\n  const history = useHistory();\n\n  const issueSearchModalHelpers = createQueryParamModalHelpers('issue-search');\n  const issueCreateModalHelpers = createQueryParamModalHelpers('issue-create');\n\n  const [{ data, error, setLocalData }, fetchProject] = useApi.get('/project');\n\n  if (!data) return <PageLoader />;\n  if (error) return <PageError />;\n\n  const { project } = data;\n\n  const updateLocalProjectIssues = (issueId, updatedFields) => {\n    setLocalData(currentData => ({\n      project: {\n        ...currentData.project,\n        issues: updateArrayItemById(currentData.project.issues, issueId, updatedFields),\n      },\n    }));\n  };\n\n  return (\n    <ProjectPage>\n      <NavbarLeft\n        issueSearchModalOpen={issueSearchModalHelpers.open}\n        issueCreateModalOpen={issueCreateModalHelpers.open}\n      />\n\n      <Sidebar project={project} />\n\n      {issueSearchModalHelpers.isOpen() && (\n        <Modal\n          isOpen\n          testid=\"modal:issue-search\"\n          variant=\"aside\"\n          width={600}\n          onClose={issueSearchModalHelpers.close}\n          renderContent={() => <IssueSearch project={project} />}\n        />\n      )}\n\n      {issueCreateModalHelpers.isOpen() && (\n        <Modal\n          isOpen\n          testid=\"modal:issue-create\"\n          width={800}\n          withCloseIcon={false}\n          onClose={issueCreateModalHelpers.close}\n          renderContent={modal => (\n            <IssueCreate\n              project={project}\n              fetchProject={fetchProject}\n              onCreate={() => history.push(`${match.url}/board`)}\n              modalClose={modal.close}\n            />\n          )}\n        />\n      )}\n\n      <Route\n        path={`${match.path}/board`}\n        render={() => (\n          <Board\n            project={project}\n            fetchProject={fetchProject}\n            updateLocalProjectIssues={updateLocalProjectIssues}\n          />\n        )}\n      />\n\n      <Route\n        path={`${match.path}/settings`}\n        render={() => <ProjectSettings project={project} fetchProject={fetchProject} />}\n      />\n\n      {match.isExact && <Redirect to={`${match.url}/board`} />}\n    </ProjectPage>\n  );\n};\n\nexport default Project;\n"
  },
  {
    "path": "client/src/browserHistory.js",
    "content": "import { createBrowserHistory } from 'history';\n\nexport default createBrowserHistory();\n"
  },
  {
    "path": "client/src/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\">\n    <title>Jira Clone</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "client/src/index.jsx",
    "content": "import 'core-js/stable';\nimport 'regenerator-runtime/runtime';\n\nimport React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport App from 'App';\n\nReactDOM.render(<App />, document.getElementById('root'));\n"
  },
  {
    "path": "client/src/shared/components/AboutTooltip/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\n\nexport const FeedbackDropdown = styled.div`\n  padding: 16px 24px 24px;\n`;\n\nexport const FeedbackImageCont = styled.div`\n  padding: 24px 56px 20px;\n`;\n\nexport const FeedbackImage = styled.img`\n  width: 100%;\n`;\n\nexport const FeedbackParagraph = styled.p`\n  margin-bottom: 12px;\n  ${font.size(15)}\n  &:last-of-type {\n    margin-bottom: 22px;\n  }\n`;\n"
  },
  {
    "path": "client/src/shared/components/AboutTooltip/index.jsx",
    "content": "import React from 'react';\n\nimport Button from 'shared/components/Button';\nimport Tooltip from 'shared/components/Tooltip';\n\nimport feedbackImage from './assets/feedback.png';\nimport { FeedbackDropdown, FeedbackImageCont, FeedbackImage, FeedbackParagraph } from './Styles';\n\nconst AboutTooltip = tooltipProps => (\n  <Tooltip\n    width={300}\n    {...tooltipProps}\n    renderContent={() => (\n      <FeedbackDropdown>\n        <FeedbackImageCont>\n          <FeedbackImage src={feedbackImage} alt=\"Give feedback\" />\n        </FeedbackImageCont>\n\n        <FeedbackParagraph>\n          This simplified Jira clone is built with React on the front-end and Node/TypeScript on the\n          back-end.\n        </FeedbackParagraph>\n\n        <FeedbackParagraph>\n          {'Read more on my website or reach out via '}\n          <a href=\"mailto:ivor@codetree.co\">\n            <strong>ivor@codetree.co</strong>\n          </a>\n        </FeedbackParagraph>\n\n        <a href=\"https://getivor.com/\" target=\"_blank\" rel=\"noreferrer noopener\">\n          <Button variant=\"primary\">Visit Website</Button>\n        </a>\n\n        <a href=\"https://github.com/oldboyxx/jira_clone\" target=\"_blank\" rel=\"noreferrer noopener\">\n          <Button style={{ marginLeft: 10 }} icon=\"github\">\n            Github Repo\n          </Button>\n        </a>\n      </FeedbackDropdown>\n    )}\n  />\n);\n\nexport default AboutTooltip;\n"
  },
  {
    "path": "client/src/shared/components/Avatar/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { font, mixin } from 'shared/utils/styles';\n\nexport const Image = styled.div`\n  display: inline-block;\n  width: ${props => props.size}px;\n  height: ${props => props.size}px;\n  border-radius: 100%;\n  ${props => mixin.backgroundImage(props.avatarUrl)}\n`;\n\nexport const Letter = styled.div`\n  display: inline-block;\n  width: ${props => props.size}px;\n  height: ${props => props.size}px;\n  border-radius: 100%;\n  text-transform: uppercase;\n  color: #fff;\n  background: ${props => props.color};\n  ${font.medium}\n  ${props => font.size(Math.round(props.size / 1.7))}\n  & > span {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    height: 100%;\n  }\n`;\n"
  },
  {
    "path": "client/src/shared/components/Avatar/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Image, Letter } from './Styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  avatarUrl: PropTypes.string,\n  name: PropTypes.string,\n  size: PropTypes.number,\n};\n\nconst defaultProps = {\n  className: undefined,\n  avatarUrl: null,\n  name: '',\n  size: 32,\n};\n\nconst Avatar = ({ className, avatarUrl, name, size, ...otherProps }) => {\n  const sharedProps = {\n    className,\n    size,\n    'data-testid': name ? `avatar:${name}` : 'avatar',\n    ...otherProps,\n  };\n\n  if (avatarUrl) {\n    return <Image avatarUrl={avatarUrl} {...sharedProps} />;\n  }\n\n  return (\n    <Letter color={getColorFromName(name)} {...sharedProps}>\n      <span>{name.charAt(0)}</span>\n    </Letter>\n  );\n};\n\nconst colors = [\n  '#DA7657',\n  '#6ADA57',\n  '#5784DA',\n  '#AA57DA',\n  '#DA5757',\n  '#DA5792',\n  '#57DACA',\n  '#57A5DA',\n];\n\nconst getColorFromName = name => colors[name.toLocaleLowerCase().charCodeAt(0) % colors.length];\n\nAvatar.propTypes = propTypes;\nAvatar.defaultProps = defaultProps;\n\nexport default Avatar;\n"
  },
  {
    "path": "client/src/shared/components/Breadcrumbs/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const Container = styled.div`\n  color: ${color.textMedium};\n  ${font.size(15)};\n`;\n\nexport const Divider = styled.span`\n  position: relative;\n  top: 2px;\n  margin: 0 10px;\n  ${font.size(18)};\n`;\n"
  },
  {
    "path": "client/src/shared/components/Breadcrumbs/index.jsx",
    "content": "import React, { Fragment } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Container, Divider } from './Styles';\n\nconst propTypes = {\n  items: PropTypes.array.isRequired,\n};\n\nconst Breadcrumbs = ({ items }) => (\n  <Container>\n    {items.map((item, index) => (\n      <Fragment key={item}>\n        {index !== 0 && <Divider>/</Divider>}\n        {item}\n      </Fragment>\n    ))}\n  </Container>\n);\n\nBreadcrumbs.propTypes = propTypes;\n\nexport default Breadcrumbs;\n"
  },
  {
    "path": "client/src/shared/components/Button/Styles.js",
    "content": "import styled, { css } from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport Spinner from 'shared/components/Spinner';\n\nexport const StyledButton = styled.button`\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  height: 32px;\n  vertical-align: middle;\n  line-height: 1;\n  padding: 0 ${props => (props.iconOnly ? 9 : 12)}px;\n  white-space: nowrap;\n  border-radius: 3px;\n  transition: all 0.1s;\n  appearance: none;\n  ${mixin.clickable}\n  ${font.size(14.5)}\n  ${props => buttonVariants[props.variant]}\n  &:disabled {\n    opacity: 0.6;\n    cursor: default;\n  }\n`;\n\nconst colored = css`\n  color: #fff;\n  background: ${props => color[props.variant]};\n  ${font.medium}\n  &:not(:disabled) {\n    &:hover {\n      background: ${props => mixin.lighten(color[props.variant], 0.15)};\n    }\n    &:active {\n      background: ${props => mixin.darken(color[props.variant], 0.1)};\n    }\n    ${props =>\n      props.isActive &&\n      css`\n        background: ${mixin.darken(color[props.variant], 0.1)} !important;\n      `}\n  }\n`;\n\nconst secondaryAndEmptyShared = css`\n  color: ${color.textDark};\n  ${font.regular}\n  &:not(:disabled) {\n    &:hover {\n      background: ${color.backgroundLight};\n    }\n    &:active {\n      color: ${color.primary};\n      background: ${color.backgroundLightPrimary};\n    }\n    ${props =>\n      props.isActive &&\n      css`\n        color: ${color.primary};\n        background: ${color.backgroundLightPrimary} !important;\n      `}\n  }\n`;\n\nconst buttonVariants = {\n  primary: colored,\n  success: colored,\n  danger: colored,\n  secondary: css`\n    background: ${color.secondary};\n    ${secondaryAndEmptyShared};\n  `,\n  empty: css`\n    background: #fff;\n    ${secondaryAndEmptyShared};\n  `,\n};\n\nexport const StyledSpinner = styled(Spinner)`\n  position: relative;\n  top: 1px;\n`;\n\nexport const Text = styled.div`\n  padding-left: ${props => (props.withPadding ? 7 : 0)}px;\n`;\n"
  },
  {
    "path": "client/src/shared/components/Button/index.jsx",
    "content": "import React, { forwardRef } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { color } from 'shared/utils/styles';\nimport Icon from 'shared/components/Icon';\n\nimport { StyledButton, StyledSpinner, Text } from './Styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  children: PropTypes.node,\n  variant: PropTypes.oneOf(['primary', 'success', 'danger', 'secondary', 'empty']),\n  icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),\n  iconSize: PropTypes.number,\n  disabled: PropTypes.bool,\n  isWorking: PropTypes.bool,\n  onClick: PropTypes.func,\n};\n\nconst defaultProps = {\n  className: undefined,\n  children: undefined,\n  variant: 'secondary',\n  icon: undefined,\n  iconSize: 18,\n  disabled: false,\n  isWorking: false,\n  onClick: () => {},\n};\n\nconst Button = forwardRef(\n  ({ children, variant, icon, iconSize, disabled, isWorking, onClick, ...buttonProps }, ref) => {\n    const handleClick = () => {\n      if (!disabled && !isWorking) {\n        onClick();\n      }\n    };\n\n    return (\n      <StyledButton\n        {...buttonProps}\n        onClick={handleClick}\n        variant={variant}\n        disabled={disabled || isWorking}\n        isWorking={isWorking}\n        iconOnly={!children}\n        ref={ref}\n      >\n        {isWorking && <StyledSpinner size={26} color={getIconColor(variant)} />}\n\n        {!isWorking && icon && typeof icon === 'string' ? (\n          <Icon type={icon} size={iconSize} color={getIconColor(variant)} />\n        ) : (\n          icon\n        )}\n        {children && <Text withPadding={isWorking || icon}>{children}</Text>}\n      </StyledButton>\n    );\n  },\n);\n\nconst getIconColor = variant =>\n  ['secondary', 'empty'].includes(variant) ? color.textDark : '#fff';\n\nButton.propTypes = propTypes;\nButton.defaultProps = defaultProps;\n\nexport default Button;\n"
  },
  {
    "path": "client/src/shared/components/ConfirmModal/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\nimport Modal from 'shared/components/Modal';\nimport Button from 'shared/components/Button';\n\nexport const StyledConfirmModal = styled(Modal)`\n  padding: 35px 40px 40px;\n`;\n\nexport const Title = styled.div`\n  padding-bottom: 25px;\n  ${font.medium}\n  ${font.size(22)}\n  line-height: 1.5;\n`;\n\nexport const Message = styled.p`\n  padding-bottom: 25px;\n  white-space: pre-wrap;\n  ${font.size(15)}\n`;\n\nexport const Actions = styled.div`\n  display: flex;\n  padding-top: 6px;\n`;\n\nexport const StyledButton = styled(Button)`\n  margin-right: 10px;\n`;\n"
  },
  {
    "path": "client/src/shared/components/ConfirmModal/index.jsx",
    "content": "import React, { Fragment, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { StyledConfirmModal, Title, Message, Actions, StyledButton } from './Styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  variant: PropTypes.oneOf(['primary', 'danger']),\n  title: PropTypes.string,\n  message: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),\n  confirmText: PropTypes.string,\n  cancelText: PropTypes.string,\n  onConfirm: PropTypes.func.isRequired,\n  renderLink: PropTypes.func.isRequired,\n};\n\nconst defaultProps = {\n  className: undefined,\n  variant: 'primary',\n  title: 'Warning',\n  message: 'Are you sure you want to continue with this action?',\n  confirmText: 'Confirm',\n  cancelText: 'Cancel',\n};\n\nconst ConfirmModal = ({\n  className,\n  variant,\n  title,\n  message,\n  confirmText,\n  cancelText,\n  onConfirm,\n  renderLink,\n}) => {\n  const [isWorking, setWorking] = useState(false);\n\n  const handleConfirm = modal => {\n    setWorking(true);\n    onConfirm({\n      close: () => {\n        modal.close();\n        setWorking(false);\n      },\n    });\n  };\n\n  return (\n    <StyledConfirmModal\n      className={className}\n      testid=\"modal:confirm\"\n      withCloseIcon={false}\n      renderLink={renderLink}\n      renderContent={modal => (\n        <Fragment>\n          <Title>{title}</Title>\n          {message && <Message>{message}</Message>}\n          <Actions>\n            <StyledButton\n              variant={variant}\n              isWorking={isWorking}\n              onClick={() => handleConfirm(modal)}\n            >\n              {confirmText}\n            </StyledButton>\n            <StyledButton hollow onClick={modal.close}>\n              {cancelText}\n            </StyledButton>\n          </Actions>\n        </Fragment>\n      )}\n    />\n  );\n};\n\nConfirmModal.propTypes = propTypes;\nConfirmModal.defaultProps = defaultProps;\n\nexport default ConfirmModal;\n"
  },
  {
    "path": "client/src/shared/components/CopyLinkButton.jsx",
    "content": "import React, { useState } from 'react';\n\nimport { copyToClipboard } from 'shared/utils/browser';\nimport { Button } from 'shared/components';\n\nconst CopyLinkButton = ({ ...buttonProps }) => {\n  const [isLinkCopied, setLinkCopied] = useState(false);\n\n  const handleLinkCopy = () => {\n    setLinkCopied(true);\n    setTimeout(() => setLinkCopied(false), 2000);\n    copyToClipboard(window.location.href);\n  };\n\n  return (\n    <Button icon=\"link\" onClick={handleLinkCopy} {...buttonProps}>\n      {isLinkCopied ? 'Link Copied' : 'Copy link'}\n    </Button>\n  );\n};\n\nexport default CopyLinkButton;\n"
  },
  {
    "path": "client/src/shared/components/DatePicker/DateSection.jsx",
    "content": "import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\nimport moment from 'moment';\nimport { times, range } from 'lodash';\n\nimport { formatDate, formatDateTimeForAPI } from 'shared/utils/dateTime';\nimport Icon from 'shared/components/Icon';\n\nimport {\n  DateSection,\n  YearSelect,\n  SelectedMonthYear,\n  Grid,\n  PrevNextIcons,\n  DayName,\n  Day,\n} from './Styles';\n\nconst propTypes = {\n  withTime: PropTypes.bool,\n  value: PropTypes.string,\n  onChange: PropTypes.func.isRequired,\n  setDropdownOpen: PropTypes.func.isRequired,\n};\n\nconst defaultProps = {\n  withTime: true,\n  value: undefined,\n};\n\nconst DatePickerDateSection = ({ withTime, value, onChange, setDropdownOpen }) => {\n  const [selectedMonth, setSelectedMonth] = useState(moment(value).startOf('month'));\n\n  const handleYearChange = year => {\n    setSelectedMonth(moment(selectedMonth).set({ year: Number(year) }));\n  };\n\n  const handleMonthChange = addOrSubtract => {\n    setSelectedMonth(moment(selectedMonth)[addOrSubtract](1, 'month'));\n  };\n\n  const handleDayChange = newDate => {\n    const existingHour = value ? moment(value).hour() : '00';\n    const existingMinute = value ? moment(value).minute() : '00';\n\n    const newDateWithExistingTime = newDate.set({\n      hour: existingHour,\n      minute: existingMinute,\n    });\n    onChange(formatDateTimeForAPI(newDateWithExistingTime));\n\n    if (!withTime) {\n      setDropdownOpen(false);\n    }\n  };\n\n  return (\n    <DateSection>\n      <SelectedMonthYear>{formatDate(selectedMonth, 'MMM YYYY')}</SelectedMonthYear>\n\n      <YearSelect onChange={event => handleYearChange(event.target.value)}>\n        {generateYearOptions().map(option => (\n          <option key={option.label} value={option.value}>\n            {option.label}\n          </option>\n        ))}\n      </YearSelect>\n\n      <PrevNextIcons>\n        <Icon type=\"arrow-left\" onClick={() => handleMonthChange('subtract')} />\n        <Icon type=\"arrow-right\" onClick={() => handleMonthChange('add')} />\n      </PrevNextIcons>\n\n      <Grid>\n        {generateWeekDayNames().map(name => (\n          <DayName key={name}>{name}</DayName>\n        ))}\n        {generateFillerDaysBeforeMonthStart(selectedMonth).map(i => (\n          <Day key={`before-${i}`} isFiller />\n        ))}\n        {generateMonthDays(selectedMonth).map(date => (\n          <Day\n            key={date}\n            isToday={moment().isSame(date, 'day')}\n            isSelected={moment(value).isSame(date, 'day')}\n            onClick={() => handleDayChange(date)}\n          >\n            {formatDate(date, 'D')}\n          </Day>\n        ))}\n        {generateFillerDaysAfterMonthEnd(selectedMonth).map(i => (\n          <Day key={`after-${i}`} isFiller />\n        ))}\n      </Grid>\n    </DateSection>\n  );\n};\n\nconst currentYear = moment().year();\n\nconst generateYearOptions = () => [\n  { label: 'Year', value: '' },\n  ...times(50, i => ({ label: `${i + currentYear - 10}`, value: `${i + currentYear - 10}` })),\n];\n\nconst generateWeekDayNames = () => moment.weekdaysMin(true);\n\nconst generateFillerDaysBeforeMonthStart = selectedMonth => {\n  const count = selectedMonth.diff(moment(selectedMonth).startOf('week'), 'days');\n  return range(count);\n};\n\nconst generateMonthDays = selectedMonth =>\n  times(selectedMonth.daysInMonth()).map(i => moment(selectedMonth).add(i, 'days'));\n\nconst generateFillerDaysAfterMonthEnd = selectedMonth => {\n  const selectedMonthEnd = moment(selectedMonth).endOf('month');\n  const weekEnd = moment(selectedMonthEnd).endOf('week');\n  const count = weekEnd.diff(selectedMonthEnd, 'days');\n  return range(count);\n};\n\nDatePickerDateSection.propTypes = propTypes;\nDatePickerDateSection.defaultProps = defaultProps;\n\nexport default DatePickerDateSection;\n"
  },
  {
    "path": "client/src/shared/components/DatePicker/Styles.js",
    "content": "import styled, { css } from 'styled-components';\n\nimport { color, font, mixin, zIndexValues } from 'shared/utils/styles';\n\nexport const StyledDatePicker = styled.div`\n  position: relative;\n`;\n\nexport const Dropdown = styled.div`\n  z-index: ${zIndexValues.dropdown};\n  position: absolute;\n  top: 130%;\n  right: 0;\n  width: 270px;\n  border-radius: 3px;\n  background: #fff;\n  ${mixin.boxShadowDropdown}\n  ${props =>\n    props.withTime &&\n    css`\n      width: 360px;\n      padding-right: 90px;\n    `}\n`;\n\nexport const DateSection = styled.div`\n  position: relative;\n  padding: 20px;\n`;\n\nexport const SelectedMonthYear = styled.div`\n  display: inline-block;\n  padding-left: 7px;\n  ${font.bold}\n  ${font.size(16)}\n`;\n\nexport const YearSelect = styled.select`\n  margin-left: 5px;\n  width: 60px;\n  height: 22px;\n  ${font.size(13)}\n`;\n\nexport const PrevNextIcons = styled.div`\n  position: absolute;\n  top: 12px;\n  right: 19px;\n  i {\n    padding: 7px 5px 4px;\n    font-size: 22px;\n    color: ${color.textLight};\n    ${mixin.clickable}\n    &:hover {\n      color: ${color.textDarkest};\n    }\n  }\n`;\n\nexport const Grid = styled.div`\n  display: flex;\n  flex-wrap: wrap;\n  padding-top: 15px;\n  text-align: center;\n`;\n\nexport const DayName = styled.div`\n  width: 14.28%;\n  height: 30px;\n  line-height: 30px;\n  color: ${color.textLight};\n  ${font.size(13)}\n`;\n\nexport const Day = styled.div`\n  width: 14.28%;\n  height: 30px;\n  line-height: 30px;\n  border-radius: 3px;\n  ${font.size(15)}\n  ${props => !props.isFiller && hoverStyles}\n  ${props => props.isToday && font.bold}\n  ${props => props.isSelected && selectedStyles}\n`;\n\nexport const TimeSection = styled.div`\n  position: absolute;\n  top: 0;\n  right: 0;\n  height: 100%;\n  width: 90px;\n  padding: 5px 0;\n  border-left: 1px solid ${color.borderLight};\n  ${mixin.scrollableY}\n`;\n\nexport const Time = styled.div`\n  padding: 5px 0 5px 20px;\n  ${font.size(14)}\n  ${props => !props.isFiller && hoverStyles}\n  ${props => props.isSelected && selectedStyles}\n`;\n\nconst hoverStyles = css`\n  ${mixin.clickable}\n  &:hover {\n    background: ${color.backgroundMedium};\n  }\n`;\n\nconst selectedStyles = css`\n  color: #fff;\n  &:hover,\n  & {\n    background: ${color.primary};\n  }\n`;\n"
  },
  {
    "path": "client/src/shared/components/DatePicker/TimeSection.jsx",
    "content": "import React, { useLayoutEffect, useRef } from 'react';\nimport PropTypes from 'prop-types';\nimport moment from 'moment';\nimport { range } from 'lodash';\n\nimport { formatDate, formatDateTimeForAPI } from 'shared/utils/dateTime';\n\nimport { TimeSection, Time } from './Styles';\n\nconst propTypes = {\n  value: PropTypes.string,\n  onChange: PropTypes.func.isRequired,\n  setDropdownOpen: PropTypes.func.isRequired,\n};\n\nconst defaultProps = {\n  value: undefined,\n};\n\nconst DatePickerTimeSection = ({ value, onChange, setDropdownOpen }) => {\n  const $sectionRef = useRef();\n\n  useLayoutEffect(() => {\n    scrollToSelectedTime($sectionRef.current, value);\n  }, [value]);\n\n  const handleTimeChange = newTime => {\n    const [newHour, newMinute] = newTime.split(':');\n\n    const existingDateWithNewTime = moment(value).set({\n      hour: Number(newHour),\n      minute: Number(newMinute),\n    });\n    onChange(formatDateTimeForAPI(existingDateWithNewTime));\n    setDropdownOpen(false);\n  };\n\n  return (\n    <TimeSection ref={$sectionRef}>\n      {generateTimes().map(time => (\n        <Time\n          key={time}\n          data-time={time}\n          isSelected={time === formatTime(value)}\n          onClick={() => handleTimeChange(time)}\n        >\n          {time}\n        </Time>\n      ))}\n    </TimeSection>\n  );\n};\n\nconst formatTime = value => formatDate(value, 'HH:mm');\n\nconst scrollToSelectedTime = ($scrollCont, value) => {\n  if (!$scrollCont) return;\n\n  const $selectedTime = $scrollCont.querySelector(`[data-time=\"${formatTime(value)}\"]`);\n  if (!$selectedTime) return;\n\n  $scrollCont.scrollTop = $selectedTime.offsetTop - 80;\n};\n\nconst generateTimes = () =>\n  range(48).map(i => {\n    const hour = `${Math.floor(i / 2)}`;\n    const paddedHour = hour.length < 2 ? `0${hour}` : hour;\n    const minute = i % 2 === 0 ? '00' : '30';\n    return `${paddedHour}:${minute}`;\n  });\n\nDatePickerTimeSection.propTypes = propTypes;\nDatePickerTimeSection.defaultProps = defaultProps;\n\nexport default DatePickerTimeSection;\n"
  },
  {
    "path": "client/src/shared/components/DatePicker/index.jsx",
    "content": "import React, { useState, useRef } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { formatDate, formatDateTime } from 'shared/utils/dateTime';\nimport useOnOutsideClick from 'shared/hooks/onOutsideClick';\nimport Input from 'shared/components/Input';\n\nimport DateSection from './DateSection';\nimport TimeSection from './TimeSection';\nimport { StyledDatePicker, Dropdown } from './Styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  withTime: PropTypes.bool,\n  value: PropTypes.string,\n  onChange: PropTypes.func.isRequired,\n};\n\nconst defaultProps = {\n  className: undefined,\n  withTime: true,\n  value: undefined,\n};\n\nconst DatePicker = ({ className, withTime, value, onChange, ...inputProps }) => {\n  const [isDropdownOpen, setDropdownOpen] = useState(false);\n  const $containerRef = useRef();\n\n  useOnOutsideClick($containerRef, isDropdownOpen, () => setDropdownOpen(false));\n\n  return (\n    <StyledDatePicker ref={$containerRef}>\n      <Input\n        icon=\"calendar\"\n        {...inputProps}\n        className={className}\n        autoComplete=\"off\"\n        value={getFormattedInputValue(value, withTime)}\n        onClick={() => setDropdownOpen(true)}\n      />\n      {isDropdownOpen && (\n        <Dropdown withTime={withTime}>\n          <DateSection\n            withTime={withTime}\n            value={value}\n            onChange={onChange}\n            setDropdownOpen={setDropdownOpen}\n          />\n          {withTime && (\n            <TimeSection value={value} onChange={onChange} setDropdownOpen={setDropdownOpen} />\n          )}\n        </Dropdown>\n      )}\n    </StyledDatePicker>\n  );\n};\n\nconst getFormattedInputValue = (value, withTime) => {\n  if (!value) return '';\n  return withTime ? formatDateTime(value) : formatDate(value);\n};\n\nDatePicker.propTypes = propTypes;\nDatePicker.defaultProps = defaultProps;\n\nexport default DatePicker;\n"
  },
  {
    "path": "client/src/shared/components/Form/Field.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { uniqueId } from 'lodash';\n\nimport Input from 'shared/components/Input';\nimport Select from 'shared/components/Select';\nimport Textarea from 'shared/components/Textarea';\nimport TextEditor from 'shared/components/TextEditor';\nimport DatePicker from 'shared/components/DatePicker';\n\nimport { StyledField, FieldLabel, FieldTip, FieldError } from './Styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  label: PropTypes.string,\n  tip: PropTypes.string,\n  error: PropTypes.string,\n  name: PropTypes.string,\n};\n\nconst defaultProps = {\n  className: undefined,\n  label: undefined,\n  tip: undefined,\n  error: undefined,\n  name: undefined,\n};\n\nconst generateField = FormComponent => {\n  const FieldComponent = ({ className, label, tip, error, name, ...otherProps }) => {\n    const fieldId = uniqueId('form-field-');\n\n    return (\n      <StyledField\n        className={className}\n        hasLabel={!!label}\n        data-testid={name ? `form-field:${name}` : 'form-field'}\n      >\n        {label && <FieldLabel htmlFor={fieldId}>{label}</FieldLabel>}\n        <FormComponent id={fieldId} invalid={!!error} name={name} {...otherProps} />\n        {tip && <FieldTip>{tip}</FieldTip>}\n        {error && <FieldError>{error}</FieldError>}\n      </StyledField>\n    );\n  };\n\n  FieldComponent.propTypes = propTypes;\n  FieldComponent.defaultProps = defaultProps;\n\n  return FieldComponent;\n};\n\nexport default {\n  Input: generateField(Input),\n  Select: generateField(Select),\n  Textarea: generateField(Textarea),\n  TextEditor: generateField(TextEditor),\n  DatePicker: generateField(DatePicker),\n};\n"
  },
  {
    "path": "client/src/shared/components/Form/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const StyledField = styled.div`\n  margin-top: 20px;\n`;\n\nexport const FieldLabel = styled.label`\n  display: block;\n  padding-bottom: 5px;\n  color: ${color.textMedium};\n  ${font.medium}\n  ${font.size(13)}\n`;\n\nexport const FieldTip = styled.div`\n  padding-top: 6px;\n  color: ${color.textMedium};\n  ${font.size(12.5)}\n`;\n\nexport const FieldError = styled.div`\n  margin-top: 6px;\n  line-height: 1;\n  color: ${color.danger};\n  ${font.medium}\n  ${font.size(12.5)}\n`;\n"
  },
  {
    "path": "client/src/shared/components/Form/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Formik, Form as FormikForm, Field as FormikField } from 'formik';\nimport { get, mapValues } from 'lodash';\n\nimport toast from 'shared/utils/toast';\nimport { is, generateErrors } from 'shared/utils/validation';\n\nimport Field from './Field';\n\nconst propTypes = {\n  validate: PropTypes.func,\n  validations: PropTypes.object,\n  validateOnBlur: PropTypes.bool,\n};\n\nconst defaultProps = {\n  validate: undefined,\n  validations: undefined,\n  validateOnBlur: false,\n};\n\nconst Form = ({ validate, validations, ...otherProps }) => (\n  <Formik\n    {...otherProps}\n    validate={values => {\n      if (validate) {\n        return validate(values);\n      }\n      if (validations) {\n        return generateErrors(values, validations);\n      }\n      return {};\n    }}\n  />\n);\n\nForm.Element = props => <FormikForm noValidate {...props} />;\n\nForm.Field = mapValues(Field, FieldComponent => ({ name, validate, ...props }) => (\n  <FormikField name={name} validate={validate}>\n    {({ field, form: { touched, errors, setFieldValue } }) => (\n      <FieldComponent\n        {...field}\n        {...props}\n        name={name}\n        error={get(touched, name) && get(errors, name)}\n        onChange={value => setFieldValue(name, value)}\n      />\n    )}\n  </FormikField>\n));\n\nForm.initialValues = (data, getFieldValues) =>\n  getFieldValues((key, defaultValue = '') => {\n    const value = get(data, key);\n    return value === undefined || value === null ? defaultValue : value;\n  });\n\nForm.handleAPIError = (error, form) => {\n  if (error.data.fields) {\n    form.setErrors(error.data.fields);\n  } else {\n    toast.error(error);\n  }\n};\n\nForm.is = is;\n\nForm.propTypes = propTypes;\nForm.defaultProps = defaultProps;\n\nexport default Form;\n"
  },
  {
    "path": "client/src/shared/components/Icon/Styles.js",
    "content": "import styled from 'styled-components';\n\nexport const StyledIcon = styled.i`\n  display: inline-block;\n  font-size: ${props => `${props.size}px`};\n  ${props =>\n    props.left || props.top ? `transform: translate(${props.left}px, ${props.top}px);` : ''}\n  &:before {\n    content: \"${props => props.code}\";\n    font-family: \"jira\" !important;\n    speak: none;\n    font-style: normal;\n    font-weight: normal;\n    font-variant: normal;\n    text-transform: none;\n    line-height: 1;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n  }\n`;\n"
  },
  {
    "path": "client/src/shared/components/Icon/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { StyledIcon } from './Styles';\n\nconst fontIconCodes = {\n  [`bug`]: '\\\\e90f',\n  [`stopwatch`]: '\\\\e914',\n  [`task`]: '\\\\e910',\n  [`story`]: '\\\\e911',\n  [`arrow-down`]: '\\\\e90a',\n  [`arrow-left-circle`]: '\\\\e917',\n  [`arrow-up`]: '\\\\e90b',\n  [`chevron-down`]: '\\\\e900',\n  [`chevron-left`]: '\\\\e901',\n  [`chevron-right`]: '\\\\e902',\n  [`chevron-up`]: '\\\\e903',\n  [`board`]: '\\\\e904',\n  [`help`]: '\\\\e905',\n  [`link`]: '\\\\e90c',\n  [`menu`]: '\\\\e916',\n  [`more`]: '\\\\e90e',\n  [`attach`]: '\\\\e90d',\n  [`plus`]: '\\\\e906',\n  [`search`]: '\\\\e907',\n  [`issues`]: '\\\\e908',\n  [`settings`]: '\\\\e909',\n  [`close`]: '\\\\e913',\n  [`feedback`]: '\\\\e918',\n  [`trash`]: '\\\\e912',\n  [`github`]: '\\\\e915',\n  [`shipping`]: '\\\\e91c',\n  [`component`]: '\\\\e91a',\n  [`reports`]: '\\\\e91b',\n  [`page`]: '\\\\e919',\n  [`calendar`]: '\\\\e91d',\n  [`arrow-left`]: '\\\\e91e',\n  [`arrow-right`]: '\\\\e91f',\n};\n\nconst propTypes = {\n  className: PropTypes.string,\n  type: PropTypes.oneOf(Object.keys(fontIconCodes)).isRequired,\n  size: PropTypes.number,\n  left: PropTypes.number,\n  top: PropTypes.number,\n};\n\nconst defaultProps = {\n  className: undefined,\n  size: 16,\n  left: 0,\n  top: 0,\n};\n\nconst Icon = ({ type, ...iconProps }) => (\n  <StyledIcon {...iconProps} data-testid={`icon:${type}`} code={fontIconCodes[type]} />\n);\n\nIcon.propTypes = propTypes;\nIcon.defaultProps = defaultProps;\n\nexport default Icon;\n"
  },
  {
    "path": "client/src/shared/components/Input/Styles.js",
    "content": "import styled, { css } from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\nimport Icon from 'shared/components/Icon';\n\nexport const StyledInput = styled.div`\n  position: relative;\n  display: inline-block;\n  height: 32px;\n  width: 100%;\n`;\n\nexport const InputElement = styled.input`\n  height: 100%;\n  width: 100%;\n  padding: 0 7px;\n  border-radius: 3px;\n  border: 1px solid ${color.borderLightest};\n  color: ${color.textDarkest};\n  background: ${color.backgroundLightest};\n  transition: background 0.1s;\n  ${font.regular}\n  ${font.size(15)}\n  ${props => props.hasIcon && 'padding-left: 32px;'}\n  &:hover {\n    background: ${color.backgroundLight};\n  }\n  &:focus {\n    background: #fff;\n    border: 1px solid ${color.borderInputFocus};\n    box-shadow: 0 0 0 1px ${color.borderInputFocus};\n  }\n  ${props =>\n    props.invalid &&\n    css`\n      &,\n      &:focus {\n        border: 1px solid ${color.danger};\n        box-shadow: none;\n      }\n    `}\n`;\n\nexport const StyledIcon = styled(Icon)`\n  position: absolute;\n  top: 8px;\n  left: 8px;\n  pointer-events: none;\n  color: ${color.textMedium};\n`;\n"
  },
  {
    "path": "client/src/shared/components/Input/index.jsx",
    "content": "import React, { forwardRef } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { StyledInput, InputElement, StyledIcon } from './Styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),\n  icon: PropTypes.string,\n  invalid: PropTypes.bool,\n  filter: PropTypes.instanceOf(RegExp),\n  onChange: PropTypes.func,\n};\n\nconst defaultProps = {\n  className: undefined,\n  value: undefined,\n  icon: undefined,\n  invalid: false,\n  filter: undefined,\n  onChange: () => {},\n};\n\nconst Input = forwardRef(({ icon, className, filter, onChange, ...inputProps }, ref) => {\n  const handleChange = event => {\n    if (!filter || filter.test(event.target.value)) {\n      onChange(event.target.value, event);\n    }\n  };\n\n  return (\n    <StyledInput className={className}>\n      {icon && <StyledIcon type={icon} size={15} />}\n      <InputElement {...inputProps} onChange={handleChange} hasIcon={!!icon} ref={ref} />\n    </StyledInput>\n  );\n});\n\nInput.propTypes = propTypes;\nInput.defaultProps = defaultProps;\n\nexport default Input;\n"
  },
  {
    "path": "client/src/shared/components/InputDebounced.jsx",
    "content": "import React, { useState, useRef, useEffect, useCallback } from 'react';\nimport PropTypes from 'prop-types';\nimport { debounce } from 'lodash';\n\nimport { Input } from 'shared/components';\n\nconst propTypes = {\n  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),\n  onChange: PropTypes.func.isRequired,\n};\n\nconst defaultProps = {\n  value: undefined,\n};\n\nconst InputDebounced = ({ onChange, value: propsValue, ...inputProps }) => {\n  const [value, setValue] = useState(propsValue);\n  const isControlled = propsValue !== undefined;\n\n  const handleChange = useCallback(\n    debounce(newValue => onChange(newValue), 500),\n    [],\n  );\n\n  const valueRef = useRef(value);\n  valueRef.current = value;\n\n  useEffect(() => {\n    if (propsValue !== valueRef.current) {\n      setValue(propsValue);\n    }\n  }, [propsValue]);\n\n  return (\n    <Input\n      {...inputProps}\n      value={isControlled ? value : undefined}\n      onChange={newValue => {\n        setValue(newValue);\n        handleChange(newValue);\n      }}\n    />\n  );\n};\n\nInputDebounced.propTypes = propTypes;\nInputDebounced.defaultProps = defaultProps;\n\nexport default InputDebounced;\n"
  },
  {
    "path": "client/src/shared/components/IssuePriorityIcon/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { issuePriorityColors } from 'shared/utils/styles';\nimport { Icon } from 'shared/components';\n\nexport const PriorityIcon = styled(Icon)`\n  color: ${props => issuePriorityColors[props.color]};\n`;\n"
  },
  {
    "path": "client/src/shared/components/IssuePriorityIcon/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { IssuePriority } from 'shared/constants/issues';\n\nimport { PriorityIcon } from './Styles';\n\nconst propTypes = {\n  priority: PropTypes.string.isRequired,\n};\n\nconst IssuePriorityIcon = ({ priority, ...otherProps }) => {\n  const iconType = [IssuePriority.LOW, IssuePriority.LOWEST].includes(priority)\n    ? 'arrow-down'\n    : 'arrow-up';\n\n  return <PriorityIcon type={iconType} color={priority} size={18} {...otherProps} />;\n};\n\nIssuePriorityIcon.propTypes = propTypes;\n\nexport default IssuePriorityIcon;\n"
  },
  {
    "path": "client/src/shared/components/IssueTypeIcon/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { issueTypeColors } from 'shared/utils/styles';\nimport { Icon } from 'shared/components';\n\nexport const TypeIcon = styled(Icon)`\n  color: ${props => issueTypeColors[props.color]};\n`;\n"
  },
  {
    "path": "client/src/shared/components/IssueTypeIcon/index.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { TypeIcon } from './Styles';\n\nconst propTypes = {\n  type: PropTypes.string.isRequired,\n};\n\nconst IssueTypeIcon = ({ type, ...otherProps }) => (\n  <TypeIcon type={type} color={type} size={18} {...otherProps} />\n);\n\nIssueTypeIcon.propTypes = propTypes;\n\nexport default IssueTypeIcon;\n"
  },
  {
    "path": "client/src/shared/components/Logo.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nconst propTypes = {\n  className: PropTypes.string,\n  size: PropTypes.number,\n};\n\nconst defaultProps = {\n  className: undefined,\n  size: 28,\n};\n\nconst Logo = ({ className, size }) => (\n  <span className={className}>\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 75.76 75.76\" width={size}>\n      <defs>\n        <linearGradient\n          id=\"linear-gradient\"\n          x1=\"34.64\"\n          y1=\"15.35\"\n          x2=\"19\"\n          y2=\"30.99\"\n          gradientUnits=\"userSpaceOnUse\"\n        >\n          <stop offset=\"0.18\" stopColor=\"rgba(0, 82, 204, 0.2)\" />\n          <stop offset=\"1\" stopColor=\"#DEEBFE\" />\n        </linearGradient>\n        <linearGradient\n          id=\"linear-gradient-2\"\n          x1=\"38.78\"\n          y1=\"60.28\"\n          x2=\"54.39\"\n          y2=\"44.67\"\n          xlinkHref=\"#linear-gradient\"\n        />\n      </defs>\n      <title>Jira Software-blue</title>\n      <g id=\"Layer_2\" data-name=\"Layer 2\">\n        <g id=\"Blue\">\n          <path\n            style={{ fill: '#DEEBFE' }}\n            d=\"M72.4,35.76,39.8,3.16,36.64,0h0L12.1,24.54h0L.88,35.76A3,3,0,0,0,.88,40L23.3,62.42,36.64,75.76,61.18,51.22l.38-.38L72.4,40A3,3,0,0,0,72.4,35.76ZM36.64,49.08l-11.2-11.2,11.2-11.2,11.2,11.2Z\"\n          />\n          <path\n            style={{ fill: 'url(#linear-gradient)' }}\n            d=\"M36.64,26.68A18.86,18.86,0,0,1,36.56.09L12.05,24.59,25.39,37.93,36.64,26.68Z\"\n          />\n          <path\n            style={{ fill: 'url(#linear-gradient-2)' }}\n            d=\"M47.87,37.85,36.64,49.08a18.86,18.86,0,0,1,0,26.68h0L61.21,51.19Z\"\n          />\n        </g>\n      </g>\n    </svg>\n  </span>\n);\n\nLogo.propTypes = propTypes;\nLogo.defaultProps = defaultProps;\n\nexport default Logo;\n"
  },
  {
    "path": "client/src/shared/components/Modal/Styles.js",
    "content": "import styled, { css } from 'styled-components';\n\nimport { color, mixin, zIndexValues } from 'shared/utils/styles';\nimport Icon from 'shared/components/Icon';\n\nexport const ScrollOverlay = styled.div`\n  z-index: ${zIndexValues.modal};\n  position: fixed;\n  top: 0;\n  left: 0;\n  height: 100%;\n  width: 100%;\n  ${mixin.scrollableY}\n`;\n\nexport const ClickableOverlay = styled.div`\n  min-height: 100%;\n  background: rgba(9, 30, 66, 0.54);\n  ${props => clickOverlayStyles[props.variant]}\n`;\n\nconst clickOverlayStyles = {\n  center: css`\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    padding: 50px;\n  `,\n  aside: '',\n};\n\nexport const StyledModal = styled.div`\n  display: inline-block;\n  position: relative;\n  width: 100%;\n  background: #fff;\n  ${props => modalStyles[props.variant]}\n`;\n\nconst modalStyles = {\n  center: css`\n    max-width: ${props => props.width}px;\n    vertical-align: middle;\n    border-radius: 3px;\n    ${mixin.boxShadowMedium}\n  `,\n  aside: css`\n    min-height: 100vh;\n    max-width: ${props => props.width}px;\n    box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);\n  `,\n};\n\nexport const CloseIcon = styled(Icon)`\n  position: absolute;\n  font-size: 25px;\n  color: ${color.textMedium};\n  transition: all 0.1s;\n  ${mixin.clickable}\n  ${props => closeIconStyles[props.variant]}\n`;\n\nconst closeIconStyles = {\n  center: css`\n    top: 10px;\n    right: 12px;\n    padding: 3px 5px 0px 5px;\n    border-radius: 4px;\n    &:hover {\n      background: ${color.backgroundLight};\n    }\n  `,\n  aside: css`\n    top: 10px;\n    right: -30px;\n    width: 50px;\n    height: 50px;\n    padding-top: 10px;\n    border-radius: 3px;\n    text-align: center;\n    background: #fff;\n    border: 1px solid ${color.borderLightest};\n    ${mixin.boxShadowMedium};\n    &:hover {\n      color: ${color.primary};\n    }\n  `,\n};\n"
  },
  {
    "path": "client/src/shared/components/Modal/index.jsx",
    "content": "import React, { Fragment, useState, useRef, useEffect, useCallback } from 'react';\nimport ReactDOM from 'react-dom';\nimport PropTypes from 'prop-types';\n\nimport useOnOutsideClick from 'shared/hooks/onOutsideClick';\nimport useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';\n\nimport { ScrollOverlay, ClickableOverlay, StyledModal, CloseIcon } from './Styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  testid: PropTypes.string,\n  variant: PropTypes.oneOf(['center', 'aside']),\n  width: PropTypes.number,\n  withCloseIcon: PropTypes.bool,\n  isOpen: PropTypes.bool,\n  onClose: PropTypes.func,\n  renderLink: PropTypes.func,\n  renderContent: PropTypes.func.isRequired,\n};\n\nconst defaultProps = {\n  className: undefined,\n  testid: 'modal',\n  variant: 'center',\n  width: 600,\n  withCloseIcon: true,\n  isOpen: undefined,\n  onClose: () => {},\n  renderLink: () => {},\n};\n\nconst Modal = ({\n  className,\n  testid,\n  variant,\n  width,\n  withCloseIcon,\n  isOpen: propsIsOpen,\n  onClose: tellParentToClose,\n  renderLink,\n  renderContent,\n}) => {\n  const [stateIsOpen, setStateOpen] = useState(false);\n  const isControlled = typeof propsIsOpen === 'boolean';\n  const isOpen = isControlled ? propsIsOpen : stateIsOpen;\n\n  const $modalRef = useRef();\n  const $clickableOverlayRef = useRef();\n\n  const closeModal = useCallback(() => {\n    if (!isControlled) {\n      setStateOpen(false);\n    } else {\n      tellParentToClose();\n    }\n  }, [isControlled, tellParentToClose]);\n\n  useOnOutsideClick($modalRef, isOpen, closeModal, $clickableOverlayRef);\n  useOnEscapeKeyDown(isOpen, closeModal);\n\n  useEffect(() => {\n    document.body.style.overflow = 'hidden';\n\n    return () => {\n      document.body.style.overflow = 'visible';\n    };\n  }, [isOpen]);\n\n  return (\n    <Fragment>\n      {!isControlled && renderLink({ open: () => setStateOpen(true) })}\n\n      {isOpen &&\n        ReactDOM.createPortal(\n          <ScrollOverlay>\n            <ClickableOverlay variant={variant} ref={$clickableOverlayRef}>\n              <StyledModal\n                className={className}\n                variant={variant}\n                width={width}\n                data-testid={testid}\n                ref={$modalRef}\n              >\n                {withCloseIcon && <CloseIcon type=\"close\" variant={variant} onClick={closeModal} />}\n                {renderContent({ close: closeModal })}\n              </StyledModal>\n            </ClickableOverlay>\n          </ScrollOverlay>,\n          $root,\n        )}\n    </Fragment>\n  );\n};\n\nconst $root = document.getElementById('root');\n\nModal.propTypes = propTypes;\nModal.defaultProps = defaultProps;\n\nexport default Modal;\n"
  },
  {
    "path": "client/src/shared/components/PageError/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font, mixin } from 'shared/utils/styles';\nimport { Icon } from 'shared/components';\n\nimport imageBackground from './assets/background-forest.jpg';\n\nexport const ErrorPage = styled.div`\n  padding: 64px;\n`;\n\nexport const ErrorPageInner = styled.div`\n  margin: 0 auto;\n  max-width: 1440px;\n  padding: 200px 0;\n  ${mixin.backgroundImage(imageBackground)}\n  @media (max-height: 680px) {\n    padding: 140px 0;\n  }\n`;\n\nexport const ErrorBox = styled.div`\n  position: relative;\n  margin: 0 auto;\n  max-width: 480px;\n  padding: 32px;\n  border-radius: 3px;\n  border: 1px solid ${color.borderLight};\n  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);\n  background: rgba(255, 255, 255, 0.9);\n`;\n\nexport const StyledIcon = styled(Icon)`\n  position: absolute;\n  top: 32px;\n  left: 32px;\n  font-size: 30px;\n  color: ${color.primary};\n`;\n\nexport const Title = styled.h1`\n  margin-bottom: 16px;\n  padding-left: 42px;\n  ${font.size(29)}\n`;\n"
  },
  {
    "path": "client/src/shared/components/PageError/index.jsx",
    "content": "import React from 'react';\n\nimport { ErrorPage, ErrorPageInner, ErrorBox, StyledIcon, Title } from './Styles';\n\nconst PageError = () => (\n  <ErrorPage>\n    <ErrorPageInner>\n      <ErrorBox>\n        <StyledIcon type=\"bug\" />\n        <Title>There’s been a glitch…</Title>\n        <p>\n          {'We’re not quite sure what went wrong. Please contact us or try looking on our '}\n          <a href=\"https://support.atlassian.com/jira-software-cloud/\">Help Center</a>\n          {' if you need a hand.'}\n        </p>\n      </ErrorBox>\n    </ErrorPageInner>\n  </ErrorPage>\n);\n\nexport default PageError;\n"
  },
  {
    "path": "client/src/shared/components/PageLoader/Styles.js",
    "content": "import styled from 'styled-components';\n\nexport default styled.div`\n  width: 100%;\n  padding: 200px 0;\n  text-align: center;\n`;\n"
  },
  {
    "path": "client/src/shared/components/PageLoader/index.jsx",
    "content": "import React from 'react';\n\nimport Spinner from 'shared/components/Spinner';\n\nimport StyledPageLoader from './Styles';\n\nconst PageLoader = () => (\n  <StyledPageLoader>\n    <Spinner size={70} />\n  </StyledPageLoader>\n);\n\nexport default PageLoader;\n"
  },
  {
    "path": "client/src/shared/components/ProjectAvatar.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nconst propTypes = {\n  className: PropTypes.string,\n  size: PropTypes.number,\n};\n\nconst defaultProps = {\n  className: undefined,\n  size: 40,\n};\n\nconst ProjectAvatar = ({ className, size }) => (\n  <span className={className}>\n    <svg\n      width={size}\n      height={size}\n      style={{ borderRadius: 3 }}\n      viewBox=\"0 0 128 128\"\n      version=\"1.1\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <defs>\n        <rect id=\"path-1\" x=\"0\" y=\"0\" width=\"128\" height=\"128\" />\n      </defs>\n      <g id=\"Page-1\" stroke=\"none\" strokeWidth=\"1\" fill=\"none\" fillRule=\"evenodd\">\n        <g id=\"project_avatar_settings\">\n          <g>\n            <mask id=\"mask-2\" fill=\"white\">\n              <use xlinkHref=\"#path-1\" />\n            </mask>\n            <use id=\"Rectangle\" fill=\"#FF5630\" xlinkHref=\"#path-1\" />\n            <g id=\"Settings\" fillRule=\"nonzero\">\n              <g transform=\"translate(20.000000, 17.000000)\">\n                <path\n                  d=\"M74.578,84.289 L72.42,84.289 C70.625,84.289 69.157,82.821 69.157,81.026 L69.157,16.537 C69.157,14.742 70.625,13.274 72.42,13.274 L74.578,13.274 C76.373,13.274 77.841,14.742 77.841,16.537 L77.841,81.026 C77.842,82.82 76.373,84.289 74.578,84.289 Z\"\n                  id=\"Shape\"\n                  fill=\"#2A5083\"\n                />\n                <path\n                  d=\"M14.252,84.289 L12.094,84.289 C10.299,84.289 8.831,82.821 8.831,81.026 L8.831,16.537 C8.831,14.742 10.299,13.274 12.094,13.274 L14.252,13.274 C16.047,13.274 17.515,14.742 17.515,16.537 L17.515,81.026 C17.515,82.82 16.047,84.289 14.252,84.289 Z\"\n                  id=\"Shape\"\n                  fill=\"#2A5083\"\n                />\n                <rect\n                  id=\"Rectangle-path\"\n                  fill=\"#153A56\"\n                  x=\"8.83\"\n                  y=\"51.311\"\n                  width=\"8.685\"\n                  height=\"7.763\"\n                />\n                <path\n                  d=\"M13.173,53.776 L13.173,53.776 C6.342,53.776 0.753,48.187 0.753,41.356 L0.753,41.356 C0.753,34.525 6.342,28.936 13.173,28.936 L13.173,28.936 C20.004,28.936 25.593,34.525 25.593,41.356 L25.593,41.356 C25.593,48.187 20.004,53.776 13.173,53.776 Z\"\n                  id=\"Shape\"\n                  fill=\"#FFFFFF\"\n                />\n                <path\n                  d=\"M18.021,43.881 L8.324,43.881 C7.453,43.881 6.741,43.169 6.741,42.298 L6.741,41.25 C6.741,40.379 7.453,39.667 8.324,39.667 L18.021,39.667 C18.892,39.667 19.604,40.379 19.604,41.25 L19.604,42.297 C19.605,43.168 18.892,43.881 18.021,43.881 Z\"\n                  id=\"Shape\"\n                  fill=\"#2A5083\"\n                  opacity=\"0.2\"\n                />\n                <rect\n                  id=\"Rectangle-path\"\n                  fill=\"#153A56\"\n                  x=\"69.157\"\n                  y=\"68.307\"\n                  width=\"8.685\"\n                  height=\"7.763\"\n                />\n                <path\n                  d=\"M73.499,70.773 L73.499,70.773 C66.668,70.773 61.079,65.184 61.079,58.353 L61.079,58.353 C61.079,51.522 66.668,45.933 73.499,45.933 L73.499,45.933 C80.33,45.933 85.919,51.522 85.919,58.353 L85.919,58.353 C85.919,65.183 80.33,70.773 73.499,70.773 Z\"\n                  id=\"Shape\"\n                  fill=\"#FFFFFF\"\n                />\n                <path\n                  d=\"M78.348,60.877 L68.651,60.877 C67.78,60.877 67.068,60.165 67.068,59.294 L67.068,58.247 C67.068,57.376 67.781,56.664 68.651,56.664 L78.348,56.664 C79.219,56.664 79.931,57.377 79.931,58.247 L79.931,59.294 C79.931,60.165 79.219,60.877 78.348,60.877 Z\"\n                  id=\"Shape\"\n                  fill=\"#2A5083\"\n                  opacity=\"0.2\"\n                />\n                <path\n                  d=\"M44.415,84.289 L42.257,84.289 C40.462,84.289 38.994,82.821 38.994,81.026 L38.994,16.537 C38.994,14.742 40.462,13.274 42.257,13.274 L44.415,13.274 C46.21,13.274 47.678,14.742 47.678,16.537 L47.678,81.026 C47.678,82.82 46.21,84.289 44.415,84.289 Z\"\n                  id=\"Shape\"\n                  fill=\"#2A5083\"\n                />\n                <rect\n                  id=\"Rectangle-path\"\n                  fill=\"#153A56\"\n                  x=\"38.974\"\n                  y=\"23.055\"\n                  width=\"8.685\"\n                  height=\"7.763\"\n                />\n                <path\n                  d=\"M43.316,25.521 L43.316,25.521 C36.485,25.521 30.896,19.932 30.896,13.101 L30.896,13.101 C30.896,6.27 36.485,0.681 43.316,0.681 L43.316,0.681 C50.147,0.681 55.736,6.27 55.736,13.101 L55.736,13.101 C55.736,19.932 50.147,25.521 43.316,25.521 Z\"\n                  id=\"Shape\"\n                  fill=\"#FFFFFF\"\n                />\n                <path\n                  d=\"M48.165,15.626 L38.468,15.626 C37.597,15.626 36.885,14.914 36.885,14.043 L36.885,12.996 C36.885,12.125 37.597,11.413 38.468,11.413 L48.165,11.413 C49.036,11.413 49.748,12.125 49.748,12.996 L49.748,14.043 C49.748,14.913 49.036,15.626 48.165,15.626 Z\"\n                  id=\"Shape\"\n                  fill=\"#2A5083\"\n                  opacity=\"0.2\"\n                />\n              </g>\n            </g>\n          </g>\n        </g>\n      </g>\n    </svg>\n  </span>\n);\n\nProjectAvatar.propTypes = propTypes;\nProjectAvatar.defaultProps = defaultProps;\n\nexport default ProjectAvatar;\n"
  },
  {
    "path": "client/src/shared/components/Select/Dropdown.jsx",
    "content": "import React, { useState, useRef, useLayoutEffect } from 'react';\nimport PropTypes from 'prop-types';\nimport { uniq } from 'lodash';\n\nimport { KeyCodes } from 'shared/constants/keyCodes';\n\nimport { ClearIcon, Dropdown, DropdownInput, Options, Option, OptionsNoResults } from './Styles';\n\nconst propTypes = {\n  dropdownWidth: PropTypes.number,\n  value: PropTypes.any,\n  isValueEmpty: PropTypes.bool.isRequired,\n  searchValue: PropTypes.string.isRequired,\n  setSearchValue: PropTypes.func.isRequired,\n  $inputRef: PropTypes.object.isRequired,\n  deactivateDropdown: PropTypes.func.isRequired,\n  options: PropTypes.array.isRequired,\n  onChange: PropTypes.func.isRequired,\n  onCreate: PropTypes.func,\n  isMulti: PropTypes.bool.isRequired,\n  withClearValue: PropTypes.bool.isRequired,\n  propsRenderOption: PropTypes.func,\n};\n\nconst defaultProps = {\n  dropdownWidth: undefined,\n  value: undefined,\n  onCreate: undefined,\n  propsRenderOption: undefined,\n};\n\nconst SelectDropdown = ({\n  dropdownWidth,\n  value,\n  isValueEmpty,\n  searchValue,\n  setSearchValue,\n  $inputRef,\n  deactivateDropdown,\n  options,\n  onChange,\n  onCreate,\n  isMulti,\n  withClearValue,\n  propsRenderOption,\n}) => {\n  const [isCreatingOption, setCreatingOption] = useState(false);\n\n  const $optionsRef = useRef();\n\n  useLayoutEffect(() => {\n    const setFirstOptionAsActive = () => {\n      const $active = getActiveOptionNode();\n      if ($active) $active.classList.remove(activeOptionClass);\n\n      if ($optionsRef.current.firstElementChild) {\n        $optionsRef.current.firstElementChild.classList.add(activeOptionClass);\n      }\n    };\n    setFirstOptionAsActive();\n  });\n\n  const selectOptionValue = optionValue => {\n    deactivateDropdown();\n    if (isMulti) {\n      onChange(uniq([...value, optionValue]));\n    } else {\n      onChange(optionValue);\n    }\n  };\n\n  const createOption = newOptionLabel => {\n    setCreatingOption(true);\n    onCreate(newOptionLabel, createdOptionValue => {\n      setCreatingOption(false);\n      selectOptionValue(createdOptionValue);\n    });\n  };\n\n  const clearOptionValues = () => {\n    $inputRef.current.value = '';\n    $inputRef.current.focus();\n    onChange(isMulti ? [] : null);\n  };\n\n  const handleInputKeyDown = event => {\n    if (event.keyCode === KeyCodes.ESCAPE) {\n      handleInputEscapeKeyDown(event);\n    } else if (event.keyCode === KeyCodes.ENTER) {\n      handleInputEnterKeyDown(event);\n    } else if (event.keyCode === KeyCodes.ARROW_DOWN || event.keyCode === KeyCodes.ARROW_UP) {\n      handleInputArrowUpOrDownKeyDown(event);\n    }\n  };\n\n  const handleInputEscapeKeyDown = event => {\n    event.nativeEvent.stopImmediatePropagation();\n    deactivateDropdown();\n  };\n\n  const handleInputEnterKeyDown = event => {\n    event.preventDefault();\n\n    const $active = getActiveOptionNode();\n    if (!$active) return;\n\n    const optionValueToSelect = $active.getAttribute('data-select-option-value');\n    const optionLabelToCreate = $active.getAttribute('data-create-option-label');\n\n    if (optionValueToSelect) {\n      selectOptionValue(optionValueToSelect);\n    } else if (optionLabelToCreate) {\n      createOption(optionLabelToCreate);\n    }\n  };\n\n  const handleInputArrowUpOrDownKeyDown = event => {\n    const $active = getActiveOptionNode();\n    if (!$active) return;\n\n    const $options = $optionsRef.current;\n    const $optionsHeight = $options.getBoundingClientRect().height;\n    const $activeHeight = $active.getBoundingClientRect().height;\n\n    if (event.keyCode === KeyCodes.ARROW_DOWN) {\n      if ($options.lastElementChild === $active) {\n        $active.classList.remove(activeOptionClass);\n        $options.firstElementChild.classList.add(activeOptionClass);\n        $options.scrollTop = 0;\n      } else {\n        $active.classList.remove(activeOptionClass);\n        $active.nextElementSibling.classList.add(activeOptionClass);\n        if ($active.offsetTop > $options.scrollTop + $optionsHeight / 1.4) {\n          $options.scrollTop += $activeHeight;\n        }\n      }\n    } else if (event.keyCode === KeyCodes.ARROW_UP) {\n      if ($options.firstElementChild === $active) {\n        $active.classList.remove(activeOptionClass);\n        $options.lastElementChild.classList.add(activeOptionClass);\n        $options.scrollTop = $options.scrollHeight;\n      } else {\n        $active.classList.remove(activeOptionClass);\n        $active.previousElementSibling.classList.add(activeOptionClass);\n        if ($active.offsetTop < $options.scrollTop + $optionsHeight / 2.4) {\n          $options.scrollTop -= $activeHeight;\n        }\n      }\n    }\n  };\n\n  const handleOptionMouseEnter = event => {\n    const $active = getActiveOptionNode();\n    if ($active) $active.classList.remove(activeOptionClass);\n    event.currentTarget.classList.add(activeOptionClass);\n  };\n\n  const getActiveOptionNode = () => $optionsRef.current.querySelector(`.${activeOptionClass}`);\n\n  const optionsFilteredBySearchValue = options.filter(option =>\n    option.label\n      .toString()\n      .toLowerCase()\n      .includes(searchValue.toLowerCase()),\n  );\n\n  const removeSelectedOptionsMulti = opts => opts.filter(option => !value.includes(option.value));\n  const removeSelectedOptionsSingle = opts => opts.filter(option => value !== option.value);\n\n  const filteredOptions = isMulti\n    ? removeSelectedOptionsMulti(optionsFilteredBySearchValue)\n    : removeSelectedOptionsSingle(optionsFilteredBySearchValue);\n\n  const isSearchValueInOptions = options.map(option => option.label).includes(searchValue);\n  const isOptionCreatable = onCreate && searchValue && !isSearchValueInOptions;\n\n  return (\n    <Dropdown width={dropdownWidth}>\n      <DropdownInput\n        type=\"text\"\n        placeholder=\"Search\"\n        ref={$inputRef}\n        autoFocus\n        onKeyDown={handleInputKeyDown}\n        onChange={event => setSearchValue(event.target.value)}\n      />\n\n      {!isValueEmpty && withClearValue && <ClearIcon type=\"close\" onClick={clearOptionValues} />}\n\n      <Options ref={$optionsRef}>\n        {filteredOptions.map(option => (\n          <Option\n            key={option.value}\n            data-select-option-value={option.value}\n            data-testid={`select-option:${option.label}`}\n            onMouseEnter={handleOptionMouseEnter}\n            onClick={() => selectOptionValue(option.value)}\n          >\n            {propsRenderOption ? propsRenderOption(option) : option.label}\n          </Option>\n        ))}\n\n        {isOptionCreatable && (\n          <Option\n            data-create-option-label={searchValue}\n            onMouseEnter={handleOptionMouseEnter}\n            onClick={() => createOption(searchValue)}\n          >\n            {isCreatingOption ? `Creating \"${searchValue}\"...` : `Create \"${searchValue}\"`}\n          </Option>\n        )}\n      </Options>\n\n      {filteredOptions.length === 0 && <OptionsNoResults>No results</OptionsNoResults>}\n    </Dropdown>\n  );\n};\n\nconst activeOptionClass = 'jira-select-option-is-active';\n\nSelectDropdown.propTypes = propTypes;\nSelectDropdown.defaultProps = defaultProps;\n\nexport default SelectDropdown;\n"
  },
  {
    "path": "client/src/shared/components/Select/Styles.js",
    "content": "import styled, { css } from 'styled-components';\n\nimport { color, font, mixin, zIndexValues } from 'shared/utils/styles';\nimport Icon from 'shared/components/Icon';\n\nexport const StyledSelect = styled.div`\n  position: relative;\n  border-radius: 4px;\n  cursor: pointer;\n  ${font.size(14)}\n  ${props => props.variant === 'empty' && `display: inline-block;`}\n  ${props =>\n    props.variant === 'normal' &&\n    css`\n      width: 100%;\n      border: 1px solid ${color.borderLightest};\n      background: ${color.backgroundLightest};\n      transition: background 0.1s;\n      &:hover {\n        background: ${color.backgroundLight};\n      }\n    `}\n  &:focus {\n    outline: none;\n    ${props =>\n      props.variant === 'normal' &&\n      css`\n        border: 1px solid ${color.borderInputFocus};\n        box-shadow: 0 0 0 1px ${color.borderInputFocus};\n        background: #fff;\n      }\n    `}\n  }\n  ${props =>\n    props.invalid &&\n    css`\n      &,\n      &:focus {\n        border: 1px solid ${color.danger};\n        box-shadow: none;\n      }\n    `}\n`;\n\nexport const ValueContainer = styled.div`\n  display: flex;\n  align-items: center;\n  width: 100%;\n  ${props =>\n    props.variant === 'normal' &&\n    css`\n      min-height: 32px;\n      padding: 5px 5px 5px 10px;\n    `}\n`;\n\nexport const ChevronIcon = styled(Icon)`\n  margin-left: auto;\n  font-size: 18px;\n  color: ${color.textMedium};\n`;\n\nexport const Placeholder = styled.div`\n  color: ${color.textLight};\n`;\n\nexport const ValueMulti = styled.div`\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n  ${props => props.variant === 'normal' && `padding-top: 5px;`}\n`;\n\nexport const ValueMultiItem = styled.div`\n  margin: 0 5px 5px 0;\n  ${mixin.tag()}\n`;\n\nexport const AddMore = styled.div`\n  display: inline-block;\n  margin-bottom: 3px;\n  padding: 3px 0;\n  ${font.size(12.5)}\n  ${mixin.link()}\n  i {\n    margin-right: 3px;\n    vertical-align: middle;\n    font-size: 14px;\n  }\n`;\n\nexport const Dropdown = styled.div`\n  z-index: ${zIndexValues.dropdown};\n  position: absolute;\n  top: 100%;\n  left: 0;\n  border-radius: 0 0 4px 4px;\n  background: #fff;\n  ${mixin.boxShadowDropdown}\n  ${props => (props.width ? `width: ${props.width}px;` : 'width: 100%;')}\n`;\n\nexport const DropdownInput = styled.input`\n  padding: 10px 14px 8px;\n  width: 100%;\n  border: none;\n  color: ${color.textDarkest};\n  background: none;\n  &:focus {\n    outline: none;\n  }\n`;\n\nexport const ClearIcon = styled(Icon)`\n  position: absolute;\n  top: 4px;\n  right: 7px;\n  padding: 5px;\n  font-size: 16px;\n  color: ${color.textMedium};\n  ${mixin.clickable}\n`;\n\nexport const Options = styled.div`\n  max-height: 200px;\n  ${mixin.scrollableY};\n  ${mixin.customScrollbar()};\n`;\n\nexport const Option = styled.div`\n  padding: 8px 14px;\n  word-break: break-word;\n  cursor: pointer;\n  &:last-of-type {\n    margin-bottom: 8px;\n  }\n  &.jira-select-option-is-active {\n    background: ${color.backgroundLightPrimary};\n  }\n`;\n\nexport const OptionsNoResults = styled.div`\n  padding: 5px 15px 15px;\n  color: ${color.textLight};\n`;\n"
  },
  {
    "path": "client/src/shared/components/Select/index.jsx",
    "content": "import React, { useState, useRef } from 'react';\nimport PropTypes from 'prop-types';\n\nimport useOnOutsideClick from 'shared/hooks/onOutsideClick';\nimport { KeyCodes } from 'shared/constants/keyCodes';\nimport Icon from 'shared/components/Icon';\n\nimport Dropdown from './Dropdown';\nimport {\n  StyledSelect,\n  ValueContainer,\n  ChevronIcon,\n  Placeholder,\n  ValueMulti,\n  ValueMultiItem,\n  AddMore,\n} from './Styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  variant: PropTypes.oneOf(['normal', 'empty']),\n  dropdownWidth: PropTypes.number,\n  name: PropTypes.string,\n  value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),\n  defaultValue: PropTypes.any,\n  placeholder: PropTypes.string,\n  invalid: PropTypes.bool,\n  options: PropTypes.array.isRequired,\n  onChange: PropTypes.func.isRequired,\n  onCreate: PropTypes.func,\n  isMulti: PropTypes.bool,\n  withClearValue: PropTypes.bool,\n  renderValue: PropTypes.func,\n  renderOption: PropTypes.func,\n};\n\nconst defaultProps = {\n  className: undefined,\n  variant: 'normal',\n  dropdownWidth: undefined,\n  name: undefined,\n  value: undefined,\n  defaultValue: undefined,\n  placeholder: 'Select',\n  invalid: false,\n  onCreate: undefined,\n  isMulti: false,\n  withClearValue: true,\n  renderValue: undefined,\n  renderOption: undefined,\n};\n\nconst Select = ({\n  className,\n  variant,\n  dropdownWidth,\n  name,\n  value: propsValue,\n  defaultValue,\n  placeholder,\n  invalid,\n  options,\n  onChange,\n  onCreate,\n  isMulti,\n  withClearValue,\n  renderValue: propsRenderValue,\n  renderOption: propsRenderOption,\n}) => {\n  const [stateValue, setStateValue] = useState(defaultValue || (isMulti ? [] : null));\n  const [isDropdownOpen, setDropdownOpen] = useState(false);\n  const [searchValue, setSearchValue] = useState('');\n\n  const isControlled = propsValue !== undefined;\n  const value = isControlled ? propsValue : stateValue;\n\n  const $selectRef = useRef();\n  const $inputRef = useRef();\n\n  const activateDropdown = () => {\n    if (isDropdownOpen) {\n      $inputRef.current.focus();\n    } else {\n      setDropdownOpen(true);\n    }\n  };\n\n  const deactivateDropdown = () => {\n    setDropdownOpen(false);\n    setSearchValue('');\n    $selectRef.current.focus();\n  };\n\n  useOnOutsideClick($selectRef, isDropdownOpen, deactivateDropdown);\n\n  const preserveValueType = newValue => {\n    const areOptionValuesNumbers = options.some(option => typeof option.value === 'number');\n\n    if (areOptionValuesNumbers) {\n      if (isMulti) {\n        return newValue.map(Number);\n      }\n      if (newValue) {\n        return Number(newValue);\n      }\n    }\n    return newValue;\n  };\n\n  const handleChange = newValue => {\n    if (!isControlled) {\n      setStateValue(preserveValueType(newValue));\n    }\n    onChange(preserveValueType(newValue));\n  };\n\n  const removeOptionValue = optionValue => {\n    handleChange(value.filter(val => val !== optionValue));\n  };\n\n  const handleFocusedSelectKeydown = event => {\n    if (isDropdownOpen) return;\n\n    if (event.keyCode === KeyCodes.ENTER) {\n      event.preventDefault();\n    }\n    if (event.keyCode !== KeyCodes.ESCAPE && event.keyCode !== KeyCodes.TAB && !event.shiftKey) {\n      setDropdownOpen(true);\n    }\n  };\n\n  const getOption = optionValue => options.find(option => option.value === optionValue);\n  const getOptionLabel = optionValue => (getOption(optionValue) || { label: '' }).label;\n\n  const isValueEmpty = isMulti ? !value.length : !getOption(value);\n\n  return (\n    <StyledSelect\n      className={className}\n      variant={variant}\n      ref={$selectRef}\n      tabIndex=\"0\"\n      onKeyDown={handleFocusedSelectKeydown}\n      invalid={invalid}\n    >\n      <ValueContainer\n        variant={variant}\n        data-testid={name ? `select:${name}` : 'select'}\n        onClick={activateDropdown}\n      >\n        {isValueEmpty && <Placeholder>{placeholder}</Placeholder>}\n\n        {!isValueEmpty && !isMulti && propsRenderValue\n          ? propsRenderValue({ value })\n          : getOptionLabel(value)}\n\n        {!isValueEmpty && isMulti && (\n          <ValueMulti variant={variant}>\n            {value.map(optionValue =>\n              propsRenderValue ? (\n                propsRenderValue({\n                  value: optionValue,\n                  removeOptionValue: () => removeOptionValue(optionValue),\n                })\n              ) : (\n                <ValueMultiItem key={optionValue} onClick={() => removeOptionValue(optionValue)}>\n                  {getOptionLabel(optionValue)}\n                  <Icon type=\"close\" size={14} />\n                </ValueMultiItem>\n              ),\n            )}\n            <AddMore>\n              <Icon type=\"plus\" />\n              Add more\n            </AddMore>\n          </ValueMulti>\n        )}\n\n        {(!isMulti || isValueEmpty) && variant !== 'empty' && (\n          <ChevronIcon type=\"chevron-down\" top={1} />\n        )}\n      </ValueContainer>\n\n      {isDropdownOpen && (\n        <Dropdown\n          dropdownWidth={dropdownWidth}\n          value={value}\n          isValueEmpty={isValueEmpty}\n          searchValue={searchValue}\n          setSearchValue={setSearchValue}\n          $selectRef={$selectRef}\n          $inputRef={$inputRef}\n          deactivateDropdown={deactivateDropdown}\n          options={options}\n          onChange={handleChange}\n          onCreate={onCreate}\n          isMulti={isMulti}\n          withClearValue={withClearValue}\n          propsRenderOption={propsRenderOption}\n        />\n      )}\n    </StyledSelect>\n  );\n};\n\nSelect.propTypes = propTypes;\nSelect.defaultProps = defaultProps;\n\nexport default Select;\n"
  },
  {
    "path": "client/src/shared/components/Spinner.jsx",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { color as colors } from 'shared/utils/styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  size: PropTypes.number,\n  color: PropTypes.string,\n};\n\nconst defaultProps = {\n  className: undefined,\n  size: 32,\n  color: colors.textMedium,\n};\n\nconst Spinner = ({ className, size, color }) => (\n  <span className={className}>\n    <svg\n      width={size}\n      height={size}\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 100 100\"\n      preserveAspectRatio=\"xMidYMid\"\n      style={{ background: '0 0' }}\n    >\n      <g>\n        <g transform=\"translate(80 50)\">\n          <circle r={8} fill={color}>\n            <animateTransform\n              attributeName=\"transform\"\n              type=\"scale\"\n              begin=\"-0.875s\"\n              values=\"1 1;1 1\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n            />\n            <animate\n              attributeName=\"fill-opacity\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n              values=\"1;0\"\n              begin=\"-0.875s\"\n            />\n          </circle>\n        </g>\n      </g>\n      <g>\n        <g transform=\"rotate(45 -50.355 121.569)\">\n          <circle r={8} fill={color} fillOpacity=\".875\">\n            <animateTransform\n              attributeName=\"transform\"\n              type=\"scale\"\n              begin=\"-0.75s\"\n              values=\"1 1;1 1\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n            />\n            <animate\n              attributeName=\"fill-opacity\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n              values=\"1;0\"\n              begin=\"-0.75s\"\n            />\n          </circle>\n        </g>\n      </g>\n      <g>\n        <g transform=\"rotate(90 -15 65)\">\n          <circle r={8} fill={color} fillOpacity=\".75\">\n            <animateTransform\n              attributeName=\"transform\"\n              type=\"scale\"\n              begin=\"-0.625s\"\n              values=\"1 1;1 1\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n            />\n            <animate\n              attributeName=\"fill-opacity\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n              values=\"1;0\"\n              begin=\"-0.625s\"\n            />\n          </circle>\n        </g>\n      </g>\n      <g>\n        <g transform=\"rotate(135 -.355 41.569)\">\n          <circle r={8} fill={color} fillOpacity=\".625\">\n            <animateTransform\n              attributeName=\"transform\"\n              type=\"scale\"\n              begin=\"-0.5s\"\n              values=\"1 1;1 1\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n            />\n            <animate\n              attributeName=\"fill-opacity\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n              values=\"1;0\"\n              begin=\"-0.5s\"\n            />\n          </circle>\n        </g>\n      </g>\n      <g>\n        <g transform=\"rotate(180 10 25)\">\n          <circle r={8} fill={color} fillOpacity=\".5\">\n            <animateTransform\n              attributeName=\"transform\"\n              type=\"scale\"\n              begin=\"-0.375s\"\n              values=\"1 1;1 1\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n            />\n            <animate\n              attributeName=\"fill-opacity\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n              values=\"1;0\"\n              begin=\"-0.375s\"\n            />\n          </circle>\n        </g>\n      </g>\n      <g>\n        <g transform=\"rotate(-135 20.355 8.431)\">\n          <circle r={8} fill={color} fillOpacity=\".375\">\n            <animateTransform\n              attributeName=\"transform\"\n              type=\"scale\"\n              begin=\"-0.25s\"\n              values=\"1 1;1 1\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n            />\n            <animate\n              attributeName=\"fill-opacity\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n              values=\"1;0\"\n              begin=\"-0.25s\"\n            />\n          </circle>\n        </g>\n      </g>\n      <g>\n        <g transform=\"rotate(-90 35 -15)\">\n          <circle r={8} fill={color} fillOpacity=\".25\">\n            <animateTransform\n              attributeName=\"transform\"\n              type=\"scale\"\n              begin=\"-0.125s\"\n              values=\"1 1;1 1\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n            />\n            <animate\n              attributeName=\"fill-opacity\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n              values=\"1;0\"\n              begin=\"-0.125s\"\n            />\n          </circle>\n        </g>\n      </g>\n      <g>\n        <g transform=\"rotate(-45 70.355 -71.569)\">\n          <circle r={8} fill={color} fillOpacity=\".125\">\n            <animateTransform\n              attributeName=\"transform\"\n              type=\"scale\"\n              begin=\"0s\"\n              values=\"1 1;1 1\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n            />\n            <animate\n              attributeName=\"fill-opacity\"\n              keyTimes=\"0;1\"\n              dur=\"1s\"\n              repeatCount=\"indefinite\"\n              values=\"1;0\"\n              begin=\"0s\"\n            />\n          </circle>\n        </g>\n      </g>\n    </svg>\n  </span>\n);\n\nSpinner.propTypes = propTypes;\nSpinner.defaultProps = defaultProps;\n\nexport default Spinner;\n"
  },
  {
    "path": "client/src/shared/components/TextEditedContent/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { font } from 'shared/utils/styles';\n\nexport const Content = styled.div`\n  padding: 0 !important;\n  ${font.size(15)}\n  ${font.regular}\n`;\n"
  },
  {
    "path": "client/src/shared/components/TextEditedContent/index.jsx",
    "content": "/* eslint-disable react/no-danger */\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport 'quill/dist/quill.snow.css';\n\nimport { Content } from './Styles';\n\nconst propTypes = {\n  content: PropTypes.string.isRequired,\n};\n\nconst TextEditedContent = ({ content, ...otherProps }) => (\n  <div className=\"ql-snow\">\n    <Content className=\"ql-editor\" dangerouslySetInnerHTML={{ __html: content }} {...otherProps} />\n  </div>\n);\n\nTextEditedContent.propTypes = propTypes;\n\nexport default TextEditedContent;\n"
  },
  {
    "path": "client/src/shared/components/TextEditor/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const EditorCont = styled.div`\n  .ql-toolbar.ql-snow {\n    border-radius: 4px 4px 0 0;\n    border: 1px solid ${color.borderLightest};\n    border-bottom: none;\n  }\n  .ql-container.ql-snow {\n    border-radius: 0 0 4px 4px;\n    border: 1px solid ${color.borderLightest};\n    border-top: none;\n    color: ${color.textDarkest};\n    ${font.size(15)}\n    ${font.regular}\n  }\n  .ql-editor {\n    min-height: 110px;\n  }\n`;\n"
  },
  {
    "path": "client/src/shared/components/TextEditor/index.jsx",
    "content": "import React, { useLayoutEffect, useRef } from 'react';\nimport PropTypes from 'prop-types';\nimport Quill from 'quill';\nimport 'quill/dist/quill.snow.css';\n\nimport { EditorCont } from './Styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  placeholder: PropTypes.string,\n  defaultValue: PropTypes.string,\n  value: PropTypes.string,\n  onChange: PropTypes.func,\n  getEditor: PropTypes.func,\n};\n\nconst defaultProps = {\n  className: undefined,\n  placeholder: undefined,\n  defaultValue: undefined,\n  value: undefined,\n  onChange: () => {},\n  getEditor: () => {},\n};\n\nconst TextEditor = ({\n  className,\n  placeholder,\n  defaultValue,\n  // we're not really feeding new value to quill instance on each render because it's too\n  // expensive, but we're still accepting 'value' prop as alias for defaultValue because\n  // other components like <Form.Field> feed their children with data via the 'value' prop\n  value: alsoDefaultValue,\n  onChange,\n  getEditor,\n}) => {\n  const $editorContRef = useRef();\n  const $editorRef = useRef();\n  const initialValueRef = useRef(defaultValue || alsoDefaultValue || '');\n\n  useLayoutEffect(() => {\n    let quill = new Quill($editorRef.current, { placeholder, ...quillConfig });\n\n    const insertInitialValue = () => {\n      quill.clipboard.dangerouslyPasteHTML(0, initialValueRef.current);\n      quill.blur();\n    };\n    const handleContentsChange = () => {\n      onChange(getHTMLValue());\n    };\n    const getHTMLValue = () => $editorContRef.current.querySelector('.ql-editor').innerHTML;\n\n    insertInitialValue();\n    getEditor({ getValue: getHTMLValue });\n\n    quill.on('text-change', handleContentsChange);\n    return () => {\n      quill.off('text-change', handleContentsChange);\n      quill = null;\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return (\n    <EditorCont className={className} ref={$editorContRef}>\n      <div ref={$editorRef} />\n    </EditorCont>\n  );\n};\n\nconst quillConfig = {\n  theme: 'snow',\n  modules: {\n    toolbar: [\n      ['bold', 'italic', 'underline', 'strike'],\n      ['blockquote', 'code-block'],\n      [{ list: 'ordered' }, { list: 'bullet' }],\n      [{ header: [1, 2, 3, 4, 5, 6, false] }],\n      [{ color: [] }, { background: [] }],\n      ['clean'],\n    ],\n  },\n};\n\nTextEditor.propTypes = propTypes;\nTextEditor.defaultProps = defaultProps;\n\nexport default TextEditor;\n"
  },
  {
    "path": "client/src/shared/components/Textarea/Styles.js",
    "content": "import styled, { css } from 'styled-components';\n\nimport { color, font } from 'shared/utils/styles';\n\nexport const StyledTextarea = styled.div`\n  display: inline-block;\n  width: 100%;\n  textarea {\n    overflow-y: hidden;\n    width: 100%;\n    padding: 8px 12px 9px;\n    border-radius: 3px;\n    border: 1px solid ${color.borderLightest};\n    color: ${color.textDarkest};\n    background: ${color.backgroundLightest};\n    ${font.regular}\n    ${font.size(15)}\n    &:focus {\n      background: #fff;\n      border: 1px solid ${color.borderInputFocus};\n      box-shadow: 0 0 0 1px ${color.borderInputFocus};\n    }\n    ${props =>\n      props.invalid &&\n      css`\n        &,\n        &:focus {\n          border: 1px solid ${color.danger};\n        }\n      `}\n  }\n`;\n"
  },
  {
    "path": "client/src/shared/components/Textarea/index.jsx",
    "content": "import React, { forwardRef } from 'react';\nimport PropTypes from 'prop-types';\nimport TextareaAutoSize from 'react-textarea-autosize';\n\nimport { StyledTextarea } from './Styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  invalid: PropTypes.bool,\n  minRows: PropTypes.number,\n  value: PropTypes.string,\n  onChange: PropTypes.func,\n};\n\nconst defaultProps = {\n  className: undefined,\n  invalid: false,\n  minRows: 2,\n  value: undefined,\n  onChange: () => {},\n};\n\nconst Textarea = forwardRef(({ className, invalid, onChange, ...textareaProps }, ref) => (\n  <StyledTextarea className={className} invalid={invalid}>\n    <TextareaAutoSize\n      {...textareaProps}\n      onChange={event => onChange(event.target.value, event)}\n      inputRef={ref || undefined}\n    />\n  </StyledTextarea>\n));\n\nTextarea.propTypes = propTypes;\nTextarea.defaultProps = defaultProps;\n\nexport default Textarea;\n"
  },
  {
    "path": "client/src/shared/components/Tooltip/Styles.js",
    "content": "import styled from 'styled-components';\n\nimport { zIndexValues, mixin } from 'shared/utils/styles';\n\nexport const StyledTooltip = styled.div`\n  z-index: ${zIndexValues.modal + 1};\n  position: fixed;\n  width: ${props => props.width}px;\n  border-radius: 3px;\n  background: #fff;\n  ${mixin.hardwareAccelerate}\n  ${mixin.boxShadowDropdown}\n`;\n"
  },
  {
    "path": "client/src/shared/components/Tooltip/index.jsx",
    "content": "import React, { Fragment, useState, useRef, useLayoutEffect } from 'react';\nimport ReactDOM from 'react-dom';\nimport PropTypes from 'prop-types';\n\nimport useOnOutsideClick from 'shared/hooks/onOutsideClick';\n\nimport { StyledTooltip } from './Styles';\n\nconst propTypes = {\n  className: PropTypes.string,\n  placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),\n  offset: PropTypes.shape({\n    top: PropTypes.number,\n    left: PropTypes.number,\n  }),\n  width: PropTypes.number.isRequired,\n  renderLink: PropTypes.func.isRequired,\n  renderContent: PropTypes.func.isRequired,\n};\n\nconst defaultProps = {\n  className: undefined,\n  placement: 'bottom',\n  offset: {\n    top: 0,\n    left: 0,\n  },\n};\n\nconst Tooltip = ({ className, placement, offset, width, renderLink, renderContent }) => {\n  const [isOpen, setIsOpen] = useState(false);\n\n  const $linkRef = useRef();\n  const $tooltipRef = useRef();\n\n  const openTooltip = () => setIsOpen(true);\n  const closeTooltip = () => setIsOpen(false);\n\n  useOnOutsideClick([$tooltipRef, $linkRef], isOpen, closeTooltip);\n\n  useLayoutEffect(() => {\n    const setTooltipPosition = () => {\n      const { top, left } = calcPosition(offset, placement, $tooltipRef, $linkRef);\n      $tooltipRef.current.style.top = `${top}px`;\n      $tooltipRef.current.style.left = `${left}px`;\n    };\n\n    if (isOpen) {\n      setTooltipPosition();\n      window.addEventListener('resize', setTooltipPosition);\n      window.addEventListener('scroll', setTooltipPosition);\n    }\n\n    return () => {\n      window.removeEventListener('resize', setTooltipPosition);\n      window.removeEventListener('scroll', setTooltipPosition);\n    };\n  }, [isOpen, offset, placement]);\n\n  return (\n    <Fragment>\n      {renderLink({ ref: $linkRef, onClick: isOpen ? closeTooltip : openTooltip })}\n\n      {isOpen &&\n        ReactDOM.createPortal(\n          <StyledTooltip className={className} ref={$tooltipRef} width={width}>\n            {renderContent({ close: closeTooltip })}\n          </StyledTooltip>,\n          $root,\n        )}\n    </Fragment>\n  );\n};\n\nconst calcPosition = (offset, placement, $tooltipRef, $linkRef) => {\n  const margin = 10;\n  const finalOffset = { ...defaultProps.offset, ...offset };\n\n  const tooltipRect = $tooltipRef.current.getBoundingClientRect();\n  const linkRect = $linkRef.current.getBoundingClientRect();\n\n  const linkCenterY = linkRect.top + linkRect.height / 2;\n  const linkCenterX = linkRect.left + linkRect.width / 2;\n\n  const placements = {\n    top: {\n      top: linkRect.top - margin - tooltipRect.height,\n      left: linkCenterX - tooltipRect.width / 2,\n    },\n    right: {\n      top: linkCenterY - tooltipRect.height / 2,\n      left: linkRect.right + margin,\n    },\n    bottom: {\n      top: linkRect.bottom + margin,\n      left: linkCenterX - tooltipRect.width / 2,\n    },\n    left: {\n      top: linkCenterY - tooltipRect.height / 2,\n      left: linkRect.left - margin - tooltipRect.width,\n    },\n  };\n  return {\n    top: placements[placement].top + finalOffset.top,\n    left: placements[placement].left + finalOffset.left,\n  };\n};\n\nconst $root = document.getElementById('root');\n\nTooltip.propTypes = propTypes;\nTooltip.defaultProps = defaultProps;\n\nexport default Tooltip;\n"
  },
  {
    "path": "client/src/shared/components/index.js",
    "content": "export { default as AboutTooltip } from './AboutTooltip';\nexport { default as Avatar } from './Avatar';\nexport { default as Button } from './Button';\nexport { default as Breadcrumbs } from './Breadcrumbs';\nexport { default as ConfirmModal } from './ConfirmModal';\nexport { default as CopyLinkButton } from './CopyLinkButton';\nexport { default as DatePicker } from './DatePicker';\nexport { default as Form } from './Form';\nexport { default as Icon } from './Icon';\nexport { default as Input } from './Input';\nexport { default as InputDebounced } from './InputDebounced';\nexport { default as IssueTypeIcon } from './IssueTypeIcon';\nexport { default as IssuePriorityIcon } from './IssuePriorityIcon';\nexport { default as Logo } from './Logo';\nexport { default as Modal } from './Modal';\nexport { default as PageError } from './PageError';\nexport { default as PageLoader } from './PageLoader';\nexport { default as ProjectAvatar } from './ProjectAvatar';\nexport { default as Select } from './Select';\nexport { default as Spinner } from './Spinner';\nexport { default as Textarea } from './Textarea';\nexport { default as TextEditedContent } from './TextEditedContent';\nexport { default as TextEditor } from './TextEditor';\nexport { default as Tooltip } from './Tooltip';\n"
  },
  {
    "path": "client/src/shared/constants/issues.js",
    "content": "export const IssueType = {\n  TASK: 'task',\n  BUG: 'bug',\n  STORY: 'story',\n};\n\nexport const IssueStatus = {\n  BACKLOG: 'backlog',\n  SELECTED: 'selected',\n  INPROGRESS: 'inprogress',\n  DONE: 'done',\n};\n\nexport const IssuePriority = {\n  HIGHEST: '5',\n  HIGH: '4',\n  MEDIUM: '3',\n  LOW: '2',\n  LOWEST: '1',\n};\n\nexport const IssueTypeCopy = {\n  [IssueType.TASK]: 'Task',\n  [IssueType.BUG]: 'Bug',\n  [IssueType.STORY]: 'Story',\n};\n\nexport const IssueStatusCopy = {\n  [IssueStatus.BACKLOG]: 'Backlog',\n  [IssueStatus.SELECTED]: 'Selected for development',\n  [IssueStatus.INPROGRESS]: 'In progress',\n  [IssueStatus.DONE]: 'Done',\n};\n\nexport const IssuePriorityCopy = {\n  [IssuePriority.HIGHEST]: 'Highest',\n  [IssuePriority.HIGH]: 'High',\n  [IssuePriority.MEDIUM]: 'Medium',\n  [IssuePriority.LOW]: 'Low',\n  [IssuePriority.LOWEST]: 'Lowest',\n};\n"
  },
  {
    "path": "client/src/shared/constants/keyCodes.js",
    "content": "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_RIGHT: 39,\n  ARROW_DOWN: 40,\n  M: 77,\n};\n"
  },
  {
    "path": "client/src/shared/constants/projects.js",
    "content": "export const ProjectCategory = {\n  SOFTWARE: 'software',\n  MARKETING: 'marketing',\n  BUSINESS: 'business',\n};\n\nexport const ProjectCategoryCopy = {\n  [ProjectCategory.SOFTWARE]: 'Software',\n  [ProjectCategory.MARKETING]: 'Marketing',\n  [ProjectCategory.BUSINESS]: 'Business',\n};\n"
  },
  {
    "path": "client/src/shared/hooks/api/index.js",
    "content": "import useQuery from './query';\nimport useMutation from './mutation';\n\n/* eslint-disable react-hooks/rules-of-hooks */\nexport default {\n  get: (...args) => useQuery(...args),\n  post: (...args) => useMutation('post', ...args),\n  put: (...args) => useMutation('put', ...args),\n  patch: (...args) => useMutation('patch', ...args),\n  delete: (...args) => useMutation('delete', ...args),\n};\n"
  },
  {
    "path": "client/src/shared/hooks/api/mutation.js",
    "content": "import { useCallback } from 'react';\n\nimport api from 'shared/utils/api';\nimport useMergeState from 'shared/hooks/mergeState';\n\nconst useMutation = (method, url) => {\n  const [state, mergeState] = useMergeState({\n    data: null,\n    error: null,\n    isWorking: false,\n  });\n\n  const makeRequest = useCallback(\n    (variables = {}) =>\n      new Promise((resolve, reject) => {\n        mergeState({ isWorking: true });\n\n        api[method](url, variables).then(\n          data => {\n            resolve(data);\n            mergeState({ data, error: null, isWorking: false });\n          },\n          error => {\n            reject(error);\n            mergeState({ error, data: null, isWorking: false });\n          },\n        );\n      }),\n    [method, url, mergeState],\n  );\n\n  return [\n    {\n      ...state,\n      [isWorkingAlias[method]]: state.isWorking,\n    },\n    makeRequest,\n  ];\n};\n\nconst isWorkingAlias = {\n  post: 'isCreating',\n  put: 'isUpdating',\n  patch: 'isUpdating',\n  delete: 'isDeleting',\n};\n\nexport default useMutation;\n"
  },
  {
    "path": "client/src/shared/hooks/api/query.js",
    "content": "import { useRef, useCallback, useEffect } from 'react';\nimport { isEqual } from 'lodash';\n\nimport api from 'shared/utils/api';\nimport useMergeState from 'shared/hooks/mergeState';\nimport useDeepCompareMemoize from 'shared/hooks/deepCompareMemoize';\n\nconst useQuery = (url, propsVariables = {}, options = {}) => {\n  const { lazy = false, cachePolicy = 'cache-first' } = options;\n\n  const wasCalled = useRef(false);\n  const propsVariablesMemoized = useDeepCompareMemoize(propsVariables);\n\n  const isSleeping = lazy && !wasCalled.current;\n  const isCacheAvailable = cache[url] && isEqual(cache[url].apiVariables, propsVariables);\n  const canUseCache = isCacheAvailable && cachePolicy !== 'no-cache' && !wasCalled.current;\n\n  const [state, mergeState] = useMergeState({\n    data: canUseCache ? cache[url].data : null,\n    error: null,\n    isLoading: !lazy && !canUseCache,\n    variables: {},\n  });\n\n  const makeRequest = useCallback(\n    newVariables => {\n      const variables = { ...state.variables, ...(newVariables || {}) };\n      const apiVariables = { ...propsVariablesMemoized, ...variables };\n\n      const skipLoading = canUseCache && cachePolicy === 'cache-first';\n\n      if (!skipLoading) {\n        mergeState({ isLoading: true, variables });\n      } else if (newVariables) {\n        mergeState({ variables });\n      }\n\n      api.get(url, apiVariables).then(\n        data => {\n          cache[url] = { data, apiVariables };\n          mergeState({ data, error: null, isLoading: false });\n        },\n        error => {\n          mergeState({ error, data: null, isLoading: false });\n        },\n      );\n\n      wasCalled.current = true;\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [propsVariablesMemoized],\n  );\n\n  useEffect(() => {\n    if (isSleeping) return;\n    if (canUseCache && cachePolicy === 'cache-only') return;\n\n    makeRequest();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [makeRequest]);\n\n  const setLocalData = useCallback(\n    getUpdatedData =>\n      mergeState(({ data }) => {\n        const updatedData = getUpdatedData(data);\n        cache[url] = { ...(cache[url] || {}), data: updatedData };\n        return { data: updatedData };\n      }),\n    [mergeState, url],\n  );\n\n  return [\n    {\n      ...state,\n      variables: { ...propsVariablesMemoized, ...state.variables },\n      setLocalData,\n    },\n    makeRequest,\n  ];\n};\n\nconst cache = {};\n\nexport default useQuery;\n"
  },
  {
    "path": "client/src/shared/hooks/currentUser.js",
    "content": "import { get } from 'lodash';\n\nimport useApi from 'shared/hooks/api';\n\nconst useCurrentUser = ({ cachePolicy = 'cache-only' } = {}) => {\n  const [{ data }] = useApi.get('/currentUser', {}, { cachePolicy });\n\n  return {\n    currentUser: get(data, 'currentUser'),\n    currentUserId: get(data, 'currentUser.id'),\n  };\n};\n\nexport default useCurrentUser;\n"
  },
  {
    "path": "client/src/shared/hooks/deepCompareMemoize.js",
    "content": "import { useRef } from 'react';\nimport { isEqual } from 'lodash';\n\nconst useDeepCompareMemoize = value => {\n  const valueRef = useRef();\n\n  if (!isEqual(value, valueRef.current)) {\n    valueRef.current = value;\n  }\n  return valueRef.current;\n};\n\nexport default useDeepCompareMemoize;\n"
  },
  {
    "path": "client/src/shared/hooks/mergeState.js",
    "content": "import { useState, useCallback } from 'react';\nimport { isFunction } from 'lodash';\n\nconst useMergeState = initialState => {\n  const [state, setState] = useState(initialState || {});\n\n  const mergeState = useCallback(newState => {\n    if (isFunction(newState)) {\n      setState(currentState => ({ ...currentState, ...newState(currentState) }));\n    } else {\n      setState(currentState => ({ ...currentState, ...newState }));\n    }\n  }, []);\n\n  return [state, mergeState];\n};\n\nexport default useMergeState;\n"
  },
  {
    "path": "client/src/shared/hooks/onEscapeKeyDown.js",
    "content": "import { useEffect } from 'react';\n\nimport { KeyCodes } from 'shared/constants/keyCodes';\n\nconst useOnEscapeKeyDown = (isListening, onEscapeKeyDown) => {\n  useEffect(() => {\n    const handleKeyDown = event => {\n      if (event.keyCode === KeyCodes.ESCAPE) {\n        onEscapeKeyDown();\n      }\n    };\n\n    if (isListening) {\n      document.addEventListener('keydown', handleKeyDown);\n    }\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [isListening, onEscapeKeyDown]);\n};\n\nexport default useOnEscapeKeyDown;\n"
  },
  {
    "path": "client/src/shared/hooks/onOutsideClick.js",
    "content": "import { useEffect, useRef } from 'react';\n\nimport useDeepCompareMemoize from 'shared/hooks/deepCompareMemoize';\n\nconst useOnOutsideClick = (\n  $ignoredElementRefs,\n  isListening,\n  onOutsideClick,\n  $listeningElementRef,\n) => {\n  const $mouseDownTargetRef = useRef();\n  const $ignoredElementRefsMemoized = useDeepCompareMemoize([$ignoredElementRefs].flat());\n\n  useEffect(() => {\n    const handleMouseDown = event => {\n      $mouseDownTargetRef.current = event.target;\n    };\n\n    const handleMouseUp = event => {\n      const isAnyIgnoredElementAncestorOfTarget = $ignoredElementRefsMemoized.some(\n        $elementRef =>\n          $elementRef.current.contains($mouseDownTargetRef.current) ||\n          $elementRef.current.contains(event.target),\n      );\n      if (event.button === 0 && !isAnyIgnoredElementAncestorOfTarget) {\n        onOutsideClick();\n      }\n    };\n\n    const $listeningElement = ($listeningElementRef || {}).current || document;\n\n    if (isListening) {\n      $listeningElement.addEventListener('mousedown', handleMouseDown);\n      $listeningElement.addEventListener('mouseup', handleMouseUp);\n    }\n    return () => {\n      $listeningElement.removeEventListener('mousedown', handleMouseDown);\n      $listeningElement.removeEventListener('mouseup', handleMouseUp);\n    };\n  }, [$ignoredElementRefsMemoized, $listeningElementRef, isListening, onOutsideClick]);\n};\n\nexport default useOnOutsideClick;\n"
  },
  {
    "path": "client/src/shared/utils/api.js",
    "content": "import axios from 'axios';\n\nimport history from 'browserHistory';\nimport toast from 'shared/utils/toast';\nimport { objectToQueryString } from 'shared/utils/url';\nimport { getStoredAuthToken, removeStoredAuthToken } from 'shared/utils/authToken';\n\nconst defaults = {\n  baseURL: process.env.API_URL || 'http://localhost:3000',\n  headers: () => ({\n    'Content-Type': 'application/json',\n    Authorization: getStoredAuthToken() ? `Bearer ${getStoredAuthToken()}` : undefined,\n  }),\n  error: {\n    code: 'INTERNAL_ERROR',\n    message: 'Something went wrong. Please check your internet connection or contact our support.',\n    status: 503,\n    data: {},\n  },\n};\n\nconst api = (method, url, variables) =>\n  new Promise((resolve, reject) => {\n    axios({\n      url: `${defaults.baseURL}${url}`,\n      method,\n      headers: defaults.headers(),\n      params: method === 'get' ? variables : undefined,\n      data: method !== 'get' ? variables : undefined,\n      paramsSerializer: objectToQueryString,\n    }).then(\n      response => {\n        resolve(response.data);\n      },\n      error => {\n        if (error.response) {\n          if (error.response.data.error.code === 'INVALID_TOKEN') {\n            removeStoredAuthToken();\n            history.push('/authenticate');\n          } else {\n            reject(error.response.data.error);\n          }\n        } else {\n          reject(defaults.error);\n        }\n      },\n    );\n  });\n\nconst optimisticUpdate = async (url, { updatedFields, currentFields, setLocalData }) => {\n  try {\n    setLocalData(updatedFields);\n    await api('put', url, updatedFields);\n  } catch (error) {\n    setLocalData(currentFields);\n    toast.error(error);\n  }\n};\n\nexport default {\n  get: (...args) => api('get', ...args),\n  post: (...args) => api('post', ...args),\n  put: (...args) => api('put', ...args),\n  patch: (...args) => api('patch', ...args),\n  delete: (...args) => api('delete', ...args),\n  optimisticUpdate,\n};\n"
  },
  {
    "path": "client/src/shared/utils/authToken.js",
    "content": "export const getStoredAuthToken = () => localStorage.getItem('authToken');\n\nexport const storeAuthToken = token => localStorage.setItem('authToken', token);\n\nexport const removeStoredAuthToken = () => localStorage.removeItem('authToken');\n"
  },
  {
    "path": "client/src/shared/utils/browser.js",
    "content": "export const getTextContentsFromHtmlString = html => {\n  const el = document.createElement('div');\n  el.innerHTML = html;\n  return el.textContent;\n};\n\nexport const copyToClipboard = value => {\n  const $textarea = document.createElement('textarea');\n  $textarea.value = value;\n  document.body.appendChild($textarea);\n  $textarea.select();\n  document.execCommand('copy');\n  document.body.removeChild($textarea);\n};\n\nexport const isFocusedElementEditable = () =>\n  !!document.activeElement.getAttribute('contenteditable') ||\n  ['TEXTAREA', 'INPUT'].includes(document.activeElement.tagName);\n"
  },
  {
    "path": "client/src/shared/utils/dateTime.js",
    "content": "import moment from 'moment';\n\nexport const formatDate = (date, format = 'MMMM D, YYYY') =>\n  date ? moment(date).format(format) : date;\n\nexport const formatDateTime = (date, format = 'MMMM D, YYYY, h:mm A') =>\n  date ? moment(date).format(format) : date;\n\nexport const formatDateTimeForAPI = date =>\n  date\n    ? moment(date)\n        .utc()\n        .format()\n    : date;\n\nexport const formatDateTimeConversational = date => (date ? moment(date).fromNow() : date);\n"
  },
  {
    "path": "client/src/shared/utils/javascript.js",
    "content": "export const moveItemWithinArray = (arr, item, newIndex) => {\n  const arrClone = [...arr];\n  const oldIndex = arrClone.indexOf(item);\n  arrClone.splice(newIndex, 0, arrClone.splice(oldIndex, 1)[0]);\n  return arrClone;\n};\n\nexport const insertItemIntoArray = (arr, item, index) => {\n  const arrClone = [...arr];\n  arrClone.splice(index, 0, item);\n  return arrClone;\n};\n\nexport const updateArrayItemById = (arr, itemId, fields) => {\n  const arrClone = [...arr];\n  const item = arrClone.find(({ id }) => id === itemId);\n  if (item) {\n    const itemIndex = arrClone.indexOf(item);\n    arrClone.splice(itemIndex, 1, { ...item, ...fields });\n  }\n  return arrClone;\n};\n\nexport const sortByNewest = (items, sortField) =>\n  items.sort((a, b) => -a[sortField].localeCompare(b[sortField]));\n"
  },
  {
    "path": "client/src/shared/utils/queryParamModal.js",
    "content": "import history from 'browserHistory';\nimport { queryStringToObject, addToQueryString, omitFromQueryString } from 'shared/utils/url';\n\nconst open = param =>\n  history.push({\n    pathname: history.location.pathname,\n    search: addToQueryString(history.location.search, { [`modal-${param}`]: true }),\n  });\n\nconst close = param =>\n  history.push({\n    pathname: history.location.pathname,\n    search: omitFromQueryString(history.location.search, [`modal-${param}`]),\n  });\n\nconst isOpen = param => !!queryStringToObject(history.location.search)[`modal-${param}`];\n\nexport const createQueryParamModalHelpers = param => ({\n  open: () => open(param),\n  close: () => close(param),\n  isOpen: () => isOpen(param),\n});\n"
  },
  {
    "path": "client/src/shared/utils/styles.js",
    "content": "import { css } from 'styled-components';\nimport Color from 'color';\n\nimport { IssueType, IssueStatus, IssuePriority } from 'shared/constants/issues';\n\nexport const color = {\n  primary: '#0052cc', // Blue\n  success: '#0B875B', // green\n  danger: '#E13C3C', // red\n  warning: '#F89C1C', // orange\n  secondary: '#F4F5F7', // light grey\n\n  textDarkest: '#172b4d',\n  textDark: '#42526E',\n  textMedium: '#5E6C84',\n  textLight: '#8993a4',\n  textLink: '#0052cc',\n\n  backgroundDarkPrimary: '#0747A6',\n  backgroundMedium: '#dfe1e6',\n  backgroundLight: '#ebecf0',\n  backgroundLightest: '#F4F5F7',\n  backgroundLightPrimary: '#D2E5FE',\n  backgroundLightSuccess: '#E4FCEF',\n\n  borderLightest: '#dfe1e6',\n  borderLight: '#C1C7D0',\n  borderInputFocus: '#4c9aff',\n};\n\nexport const issueTypeColors = {\n  [IssueType.TASK]: '#4FADE6', // blue\n  [IssueType.BUG]: '#E44D42', // red\n  [IssueType.STORY]: '#65BA43', // green\n};\n\nexport const issuePriorityColors = {\n  [IssuePriority.HIGHEST]: '#CD1317', // red\n  [IssuePriority.HIGH]: '#E9494A', // orange\n  [IssuePriority.MEDIUM]: '#E97F33', // orange\n  [IssuePriority.LOW]: '#2D8738', // green\n  [IssuePriority.LOWEST]: '#57A55A', // green\n};\n\nexport const issueStatusColors = {\n  [IssueStatus.BACKLOG]: color.textDark,\n  [IssueStatus.INPROGRESS]: '#fff',\n  [IssueStatus.SELECTED]: color.textDark,\n  [IssueStatus.DONE]: '#fff',\n};\n\nexport const issueStatusBackgroundColors = {\n  [IssueStatus.BACKLOG]: color.backgroundMedium,\n  [IssueStatus.INPROGRESS]: color.primary,\n  [IssueStatus.SELECTED]: color.backgroundMedium,\n  [IssueStatus.DONE]: color.success,\n};\n\nexport const sizes = {\n  appNavBarLeftWidth: 64,\n  secondarySideBarWidth: 230,\n  minViewportWidth: 1000,\n};\n\nexport const zIndexValues = {\n  modal: 1000,\n  dropdown: 101,\n  navLeft: 100,\n};\n\nexport const font = {\n  regular: 'font-family: \"CircularStdBook\"; font-weight: normal;',\n  medium: 'font-family: \"CircularStdMedium\"; font-weight: normal;',\n  bold: 'font-family: \"CircularStdBold\"; font-weight: normal;',\n  black: 'font-family: \"CircularStdBlack\"; font-weight: normal;',\n  size: size => `font-size: ${size}px;`,\n};\n\nexport const mixin = {\n  darken: (colorValue, amount) =>\n    Color(colorValue)\n      .darken(amount)\n      .string(),\n  lighten: (colorValue, amount) =>\n    Color(colorValue)\n      .lighten(amount)\n      .string(),\n  rgba: (colorValue, opacity) =>\n    Color(colorValue)\n      .alpha(opacity)\n      .string(),\n  boxShadowMedium: css`\n    box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.1);\n  `,\n  boxShadowDropdown: css`\n    box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.31) 0px 0px 1px;\n  `,\n  truncateText: css`\n    overflow: hidden;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n  `,\n  clickable: css`\n    cursor: pointer;\n    user-select: none;\n  `,\n  hardwareAccelerate: css`\n    transform: translateZ(0);\n  `,\n  cover: css`\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n  `,\n  placeholderColor: colorValue => css`\n    ::-webkit-input-placeholder {\n      color: ${colorValue} !important;\n      opacity: 1 !important;\n    }\n    :-moz-placeholder {\n      color: ${colorValue} !important;\n      opacity: 1 !important;\n    }\n    ::-moz-placeholder {\n      color: ${colorValue} !important;\n      opacity: 1 !important;\n    }\n    :-ms-input-placeholder {\n      color: ${colorValue} !important;\n      opacity: 1 !important;\n    }\n  `,\n  scrollableY: css`\n    overflow-x: hidden;\n    overflow-y: auto;\n    -webkit-overflow-scrolling: touch;\n  `,\n  customScrollbar: ({ width = 8, background = color.backgroundMedium } = {}) => css`\n    &::-webkit-scrollbar {\n      width: ${width}px;\n    }\n    &::-webkit-scrollbar-track {\n      background: none;\n    }\n    &::-webkit-scrollbar-thumb {\n      border-radius: 99px;\n      background: ${background};\n    }\n  `,\n  backgroundImage: imageURL => css`\n    background-image: url(\"${imageURL}\");\n    background-position: 50% 50%;\n    background-repeat: no-repeat;\n    background-size: cover;\n    background-color: ${color.backgroundLight};\n  `,\n  link: (colorValue = color.textLink) => css`\n    cursor: pointer;\n    color: ${colorValue};\n    ${font.medium}\n    &:hover, &:visited, &:active {\n      color: ${colorValue};\n    }\n    &:hover {\n      text-decoration: underline;\n    }\n  `,\n  tag: (background = color.backgroundMedium, colorValue = color.textDarkest) => css`\n    display: inline-flex;\n    align-items: center;\n    height: 24px;\n    padding: 0 8px;\n    border-radius: 4px;\n    cursor: pointer;\n    user-select: none;\n    color: ${colorValue};\n    background: ${background};\n    ${font.bold}\n    ${font.size(12)}\n    i {\n      margin-left: 4px;\n    }\n  `,\n};\n"
  },
  {
    "path": "client/src/shared/utils/toast.js",
    "content": "import pubsub from 'sweet-pubsub';\nimport { get } from 'lodash';\n\nconst show = toast => pubsub.emit('toast', toast);\n\nconst success = title => show({ title });\n\nconst error = err => {\n  show({\n    type: 'danger',\n    title: 'Error',\n    message: get(err, 'message', err),\n    duration: 0,\n  });\n};\n\nexport default { show, error, success };\n"
  },
  {
    "path": "client/src/shared/utils/url.js",
    "content": "import queryString from 'query-string';\nimport { omit } from 'lodash';\n\nexport const queryStringToObject = (str, options = {}) =>\n  queryString.parse(str, {\n    arrayFormat: 'bracket',\n    ...options,\n  });\n\nexport const objectToQueryString = (obj, options = {}) =>\n  queryString.stringify(obj, {\n    arrayFormat: 'bracket',\n    ...options,\n  });\n\nexport const omitFromQueryString = (str, keys) =>\n  objectToQueryString(omit(queryStringToObject(str), keys));\n\nexport const addToQueryString = (str, fields) =>\n  objectToQueryString({\n    ...queryStringToObject(str),\n    ...fields,\n  });\n"
  },
  {
    "path": "client/src/shared/utils/validation.js",
    "content": "export const is = {\n  match: (testFn, message = '') => (value, fieldValues) => !testFn(value, fieldValues) && message,\n\n  required: () => value => isNilOrEmptyString(value) && 'This field is required',\n\n  minLength: min => value => !!value && value.length < min && `Must be at least ${min} characters`,\n\n  maxLength: max => value => !!value && value.length > max && `Must be at most ${max} characters`,\n\n  notEmptyArray: () => value =>\n    Array.isArray(value) && value.length === 0 && 'Please add at least one item',\n\n  email: () => value => !!value && !/.+@.+\\..+/.test(value) && 'Must be a valid email',\n\n  url: () => value =>\n    !!value &&\n    // eslint-disable-next-line no-useless-escape\n    !/^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$/.test(value) &&\n    'Must be a valid URL',\n};\n\nconst isNilOrEmptyString = value => value === undefined || value === null || value === '';\n\nexport const generateErrors = (fieldValues, fieldValidators) => {\n  const errors = {};\n\n  Object.entries(fieldValidators).forEach(([fieldName, validators]) => {\n    [validators].flat().forEach(validator => {\n      const errorMessage = validator(fieldValues[fieldName], fieldValues);\n      if (errorMessage && !errors[fieldName]) {\n        errors[fieldName] = errorMessage;\n      }\n    });\n  });\n  return errors;\n};\n"
  },
  {
    "path": "client/webpack.config.js",
    "content": "const path = require('path');\nconst webpack = require('webpack');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\n\nmodule.exports = {\n  mode: 'development',\n  entry: path.join(__dirname, 'src/index.jsx'),\n  output: {\n    filename: '[name].js',\n    path: path.resolve(__dirname, 'dev'),\n    publicPath: '/',\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.jsx?$/,\n        exclude: /node_modules/,\n        use: ['babel-loader'],\n      },\n      {\n        test: /\\.css$/,\n        use: ['style-loader', { loader: 'css-loader' }],\n      },\n      {\n        test: /\\.(jpe?g|png|gif|woff2?|eot|ttf|otf|svg)$/,\n        use: [\n          {\n            loader: 'url-loader',\n            options: { limit: 15000 },\n          },\n        ],\n      },\n    ],\n  },\n  resolve: {\n    // allows us to do absolute imports from \"src\"\n    modules: [path.join(__dirname, 'src'), 'node_modules'],\n    extensions: ['*', '.js', '.jsx'],\n  },\n  devtool: 'eval-source-map',\n  devServer: {\n    contentBase: path.join(__dirname, 'dev'),\n    historyApiFallback: true,\n    hot: true,\n  },\n  plugins: [\n    new webpack.HotModuleReplacementPlugin(),\n    new HtmlWebpackPlugin({\n      template: path.join(__dirname, 'src/index.html'),\n      favicon: path.join(__dirname, 'src/favicon.png'),\n    }),\n  ],\n};\n"
  },
  {
    "path": "client/webpack.config.production.js",
    "content": "const path = require('path');\nconst webpack = require('webpack');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\n\nmodule.exports = {\n  mode: 'production',\n  entry: {\n    main: path.join(__dirname, 'src/index.jsx'),\n  },\n  output: {\n    path: path.resolve(__dirname, 'build'),\n    filename: '[name]-[hash].js',\n    publicPath: '/',\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.jsx?$/,\n        exclude: /node_modules/,\n        use: ['babel-loader'],\n      },\n      {\n        test: /\\.css$/,\n        use: [\n          'style-loader',\n          {\n            loader: 'css-loader',\n            options: { sourceMap: true },\n          },\n        ],\n      },\n      {\n        test: /\\.(jpe?g|png|gif|svg)$/,\n        use: [\n          {\n            loader: 'url-loader',\n            options: { name: '[name]-[hash].[ext]', limit: 10000 },\n          },\n        ],\n      },\n      {\n        test: /\\.(woff2?|eot|ttf|otf)$/,\n        use: [\n          {\n            loader: 'file-loader',\n            options: { name: '[name]-[hash].[ext]' },\n          },\n        ],\n      },\n    ],\n  },\n  resolve: {\n    modules: [path.join(__dirname, 'src'), 'node_modules'],\n    extensions: ['*', '.js', '.jsx', '.css'],\n  },\n  plugins: [\n    new HtmlWebpackPlugin({\n      template: path.join(__dirname, 'src/index.html'),\n      favicon: path.join(__dirname, 'src/favicon.png'),\n    }),\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: JSON.stringify('production'),\n        API_URL: JSON.stringify('https://jira-api.ivorreic.com'),\n      },\n    }),\n    new webpack.IgnorePlugin(/^\\.\\/locale$/, /moment$/),\n  ],\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"jira_clone\",\n  \"version\": \"1.0.0\",\n  \"author\": \"Ivor Reic\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"pre-commit\": \"cd api && npm run pre-commit && cd ../client && npm run pre-commit\",\n    \"install-dependencies\": \"npm install && cd api && npm install && cd ../client && npm install\",\n    \"build\": \"cd api && npm run build && cd ../client && npm run build\",\n    \"start:production\": \"cd api && npm run start:production && cd ../client && npm run start:production\"\n  },\n  \"devDependencies\": {\n    \"husky\": \"^4.0.0-beta.5\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"npm run pre-commit\"\n    }\n  },\n  \"dependencies\": {}\n}\n"
  }
]