[
  {
    "path": ".babelrc",
    "content": "{\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", { \"legacy\": true }]\n  ]\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "root=true\n\n[*]\nend_of_line = lf\ntab_width = 2\nindent_style = space\ninsert_final_newline = true\ncharset = utf-8\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".eslintignore",
    "content": ".cache/\ndist/\nnode_modules/"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"extends\": [\n    // Enable eslint recommended, and the few overrides necessary to work with TS\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/eslint-recommended\",\n\n    // Enable recommended rules specific to TS\n    \"plugin:@typescript-eslint/recommended\",\n\n    // Enable rules that are types aware\n    \"plugin:@typescript-eslint/recommended-requiring-type-checking\",\n\n    // React rules\n    \"plugin:react/recommended\"\n  ],\n  \"parser\": \"@typescript-eslint/parser\",\n  \"plugins\": [\"@typescript-eslint\"],\n  \"parserOptions\": {\n    \"ecmaFeatures\": {\n      \"jsx\": true\n    },\n    \"project\": \"./tsconfig.json\"\n  },\n  \"settings\": {\n    \"react\": {\n      \"version\": \"detect\"\n    }\n  },\n  \"rules\": {\n    // Enforce `I` prefix for interfaces, as I like it that way\n    \"@typescript-eslint/interface-name-prefix\": [\n      \"error\",\n      {\n        \"prefixWithI\": \"always\",\n        \"allowUnderscorePrefix\": true\n      }\n    ],\n\n    // Allow using `any`, as it's sometimes easier with external API\n    \"@typescript-eslint/no-explicit-any\": \"off\",\n\n    // Do not require return types on all functions, as the inference engine is good enough to figure it out\n    \"@typescript-eslint/explicit-function-return-type\": \"off\",\n\n    // It is convenient being able to declare functions after they're used\n    \"@typescript-eslint/no-use-before-define\": \"off\",\n\n    // It is erroring when declaring global functions on the window object, which is annoying\n    \"@typescript-eslint/unbound-method\": [\"off\"],\n\n    \"@typescript-eslint/camelcase\": [\"error\", {\n      \"ignoreDestructuring\": true,\n      \"properties\": \"never\"\n    }]\n  }\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Lint & Tests\non: [push]\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v1\n        with:\n          fetch-depth: 1\n\n      - name: Restore node_modules folder\n        uses: actions/cache@v1\n        with:\n          path: node_modules\n          key: ${{ runner.os }}-cache-${{ hashFiles('**/yarn.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-cache-\n\n      - name: Install dependencies\n        run: yarn install\n\n      - name: Run ESLint\n        run: yarn lint\n\n      - name: Run tsc\n        run: tsc --noEmit\n\n      - name: Run Jest\n        run: yarn test\n\n\n  integration-tests:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v1\n        with:\n          fetch-depth: 1\n\n      - name: Run Cypress\n        uses: cypress-io/github-action@v5\n        with:\n          record: true\n          start: yarn start\n        env:\n          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}\n          CYPRESS_GITHUB_TOKEN: ${{ secrets.CYPRESS_GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\n\n.idea/\n\nyarn-error.log\n\n.cache/\n.parcel-cache/\n\ndist/\ndist.crx\ndist.pem\noctolenses-*\n\ncypress/videos\n"
  },
  {
    "path": ".nvmrc",
    "content": "20\n"
  },
  {
    "path": ".parcelrc",
    "content": "{\n  \"extends\": \"@parcel/config-default\",\n  \"transformers\": {\n    \"*.{ts,tsx}\": [\"@parcel/transformer-typescript-tsc\"]\n  }\n}"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"es5\"\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Renan GEHAN\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![](.github/icons/chrome.png)](https://chrome.google.com/webstore/detail/octolenses/ghlblfakaklgkdmfejdlffbmpcaidoci)\n[![](.github/icons/firefox.png)](https://addons.mozilla.org/firefox/addon/github-octolenses/)\n\n![](https://github.com/rgehan/octolenses/workflows/Lint%20%26%20Tests/badge.svg)\n\n# OctoLenses Browser Extension\n\n> Watch your repos and discover awesome things directly from your New Tab page.\n\nAs a developer, you shouldn't have to worry about that and instead focus on what\nis fundamental: your code\n\nThis extension allows you to create very precise filters that will nicely lay\nout all the information you need in order to be as productive as possible.\n\n![](.github/screenshots/light/dashboard.png)\n\n![](.github/screenshots/light/filter-add.png)\n\n![](.github/screenshots/light/filter-edit.png)\n\n## Installation\n\n[![](.github/icons/chrome.png)](https://chrome.google.com/webstore/detail/octolenses/ghlblfakaklgkdmfejdlffbmpcaidoci)\n[![](.github/icons/firefox.png)](https://addons.mozilla.org/firefox/addon/github-octolenses/)\n\nSimply download it on your favorite browser's extensions store.\n\nIt is available on both [Google Chrome](https://chrome.google.com/webstore/detail/octolenses/ghlblfakaklgkdmfejdlffbmpcaidoci) and [Firefox](https://addons.mozilla.org/firefox/addon/github-octolenses/).\n\nBy default, it overrides your browser's default \"New Tab\" page, but this can be\ndisabled so that it only opens when you click on the extension's icon.\n\nYou can access this setting in the settings modal, which can be opened by\nsimply clicking on the little cog icon on the top right hand corner.\n\n## Usage example\n\nAt my current job, we have quite a lovely system where each Pull Request is\nassigned a specific label depending on whether it's a WIP, under review or\nif it has been successfully (or not) reviewed.\n\nI built this tool for the very purpose of keeping track of this, but this is not\nall it can do. It can do much more, such as:\n\n- Helping you contribute to Open Source by presenting you with issues that are\n  labelled `Good first issue` or `Help wanted`\n- Helping you stay up to date with your favorite framework changes\n- Allowing you to discover trendy repositories in your favorite language\n  (similar to what [GitHunt](https://github.com/kamranahmedse/githunt) does)\n\n## Dark theme\n\nBecause being flashed bright lights in the eyes at night is the worst thing\never, I even included a pretty cool dark mode.\n\n![](.github/screenshots/dark/dashboard.png)\n\n![](.github/screenshots/dark/filter-edit.png)\n\n![](.github/screenshots/dark/discover.png)\n\n![](.github/screenshots/dark/settings-night-mode.png)\n\n## Extensively configurable\n\nThere are a lot of settings you can tweak, to adapt the experience of the\nextension to your needs.\n\n![](.github/screenshots/light/settings-cache.png)\n\n![](.github/screenshots/light/settings-git-hub.png)\n\n![](.github/screenshots/light/settings-jira.png)\n\n## Permissions asked\n\nOctoLenses only asks for the `tabs` permission, as it needs to be able to:\n\n- Detect when a tab is opened so it can eventually override it\n- Create a new tab when the extension's icon is clicked.\n\n## Development setup\n\nYou need a few tools before being able to build the extension:\n\n- `yarn`, a [JS package manager](https://yarnpkg.com/docs/install) (on Mac: `brew install yarn`)\n- `jq`, a [JSON CLI utility](https://stedolan.github.io/jq/) (on Mac: `brew install jq`).\n- `sed`, should be available on any Unix system.\n- `zip`, should be available on any Unix system.\n\nThen you can follow this process to develop/build the extension:\n\n```sh\n# Clone the repository\ngit clone git@github.com:rgehan/octolenses.git && \\\ncd octolenses\n\n# Install the dependencies\nyarn\n\n# Run the development environment...\nyarn start\n\n# ...or build the extension\nyarn build\n```\n\nThe built extension (located in the `dist/` folder), can then be loaded inside\nyour browser as an _unpacked extension_, provided you're in developer mode.\n\n[https://github.com/rgehan/octolenses](rgehan/octolenses)\n\n## Testing\n\nThe extension is covered by unit tests, and integration tests.\n\nHere is how you can run them:\n```bash\n# Run the unit tests (w/ Jest)\nyarn test\n\n# Open the integration tests runner (w/ Cypress)\nCYPRESS_GITHUB_TOKEN=<github token> yarn e2e\n```\n\n## Releasing\n\n```sh\n# Update changelog, increment version number & create release commit and tag\nyarn release patch|minor|major\n\n# Build the release .zip archive\nyarn build\n```\n\nThe archive can then be uploaded on the Chrome Store dashboard.\n\n## Contributing\n\n1. Fork it (<https://github.com/rgehan/octolenses/fork>)\n2. Create your feature branch (`git checkout -b feature/fooBar`)\n3. Commit your changes (`git commit -am 'Add some fooBar'`)\n4. Push to the branch (`git push origin feature/fooBar`)\n5. Create a new Pull Request\n\n## License\n\nMIT © Renan GEHAN\n"
  },
  {
    "path": "changelog.md",
    "content": "### v2.3.0\n- Allow filtering PRs/issues by relative update/creation date [Renan GEHAN]\n\n### v2.2.2\n- Refresh all button, when pressing Meta key [Renan GEHAN]\n- Handle draft PRs [Renan GEHAN]\n- Fix build script to avoid weird parcel issues [Renan GEHAN]\n\n### v2.2.1\n- Add support for draft PRs [Renan GEHAN]\n\n### v2.2.0\n- Fix wrap issues with GH label badges [Renan GEHAN]\n- Add .idea to .gitignore [Renan GEHAN]\n- Update main deps (parcel, tailwind) + node LTS (#219) [GitHub]\n\n### v2.1.2\n- Remove unused `storage` permission [Renan GEHAN]\n\n### v2.1.1\n- Remove unnecessary `tabs` permission [Renan GEHAN]\n\n### v2.1.0\n- use-page-overrides-api (#218) [GitHub]\n\n### v2.0.0\n- Fallback to localStorage if chrome.storage not available [Renan GEHAN]\n- Upgrade cypress [Renan GEHAN]\n- Upgrade cypress github action [Renan GEHAN]\n- Fix tsc issues [Renan GEHAN]\n- Prettify [Renan GEHAN]\n- Fix .gitignore [Renan GEHAN]\n- Remove some usage of `localStorage` [Renan GEHAN]\n- Update @types/chrome [Renan GEHAN]\n- Migrate to manifest v3 [Renan GEHAN]\n- Upgrade Cypress [Renan GEHAN]\n\n### v1.2.7\n- Upgrade all dependencies [Renan GEHAN]\n- Fix predicates deletion [Renan GEHAN]\n- [Security] Bump acorn from 5.7.3 to 5.7.4 (#161) [GitHub]\n- Bump @babel/core from 7.7.4 to 7.8.7 (#160) [GitHub]\n- Bump ts-jest from 24.2.0 to 24.3.0 (#158) [GitHub]\n- Bump @babel/polyfill from 7.7.0 to 7.8.3 (#154) [GitHub]\n- Bump typescript from 3.7.4 to 3.8.3 (#156) [GitHub]\n- Fix lint [Renan Gehan]\n\n### v1.2.6\n- Display last activity date in IssueCard [Renan Gehan]\n- Bump rimraf from 3.0.0 to 3.0.2 (#150) [GitHub]\n- Bump @typescript-eslint/parser from 2.8.0 to 2.19.2 (#152) [GitHub]\n\n### v1.2.5\n- Fix Jira resource style in night mode [Renan GEHAN]\n- Fix link style [Renan GEHAN]\n- Add support project predicate (#151) [GitHub]\n- Bump eslint-plugin-react from 7.16.0 to 7.18.3 (#147) [GitHub]\n- Bump @babel/plugin-proposal-object-rest-spread from 7.7.4 to 7.8… (#145) [GitHub]\n- Bump cypress from 3.6.1 to 3.8.3 (#146) [GitHub]\n- Bump mobx from 5.15.0 to 5.15.1 (#137) [Renan GEHAN]\n- Bump sass from 1.23.7 to 1.24.2 (#140) [Renan GEHAN]\n- Bump typescript from 3.7.2 to 3.7.4 (#139) [Renan GEHAN]\n- Bump timeago.js from 3.0.2 to 4.0.1 (#131) [Renan GEHAN]\n- Bump object-hash from 2.0.0 to 2.0.1 (#133) [Renan GEHAN]\n- [Security] Bump serialize-to-js from 3.0.0 to 3.0.2 (#136) [Renan GEHAN]\n- Bump eslint from 6.7.0 to 6.7.2 (#134) [Renan GEHAN]\n\n### v1.2.4\n- Fix \"Open in New Tab\" in recent versions of Chrome [Renan GEHAN]\n- add-tsc-check (#130) [GitHub]\n- migrate-to-eslint (#129) [GitHub]\n- improve-tests (#128) [GitHub]\n- Remove forgotten it.only() [Renan GEHAN]\n- Throw if the CYPRESS_GITHUB_TOKEN env var isn't defined [Renan GEHAN]\n- Cypress tests 2 (#127) [GitHub]\n- Bump immer from 1.12.1 to 5.0.0 (#123) [Renan GEHAN]\n- Bump @babel/polyfill from 7.6.0 to 7.7.0 (#124) [Renan GEHAN]\n- Document cypress tests [Renan GEHAN]\n- Bump react from 16.11.0 to 16.12.0 (#125) [Renan GEHAN]\n- Cypress tests (#126) [GitHub]\n- Bump @babel/core from 7.6.4 to 7.7.2 (#113) [Renan GEHAN]\n- Bump @types/jest from 24.0.21 to 24.0.22 (#118) [Renan GEHAN]\n- Bump @babel/plugin-proposal-class-properties from 7.5.5 to 7.7.0 (#115) [Renan GEHAN]\n- Bump tslint from 5.20.0 to 5.20.1 (#117) [Renan GEHAN]\n- Bump rimraf from 2.7.1 to 3.0.0 (#114) [Renan GEHAN]\n- Bump mobx from 5.14.2 to 5.15.0 (#116) [Renan GEHAN]\n- Bump @types/lodash from 4.14.144 to 4.14.146 (#119) [Renan GEHAN]\n- Bump object-hash from 1.3.1 to 2.0.0 (#120) [Renan GEHAN]\n- Bump @types/react-sortable-hoc from 0.6.6 to 0.7.1 (#121) [Renan GEHAN]\n- Bump prettier from 1.18.2 to 1.19.1 (#122) [Renan GEHAN]\n\n### v1.2.3\n- Fix URL of the jira issue cards [Renan GEHAN]\n- Upgrade all dependencies [Renan GEHAN]\n- Bump tslint-react from 4.0.0 to 4.1.0 (#102) [Renan GEHAN]\n- Bump human-format from 0.10.0 to 0.10.1 [Renan GEHAN]\n- Bump tslint from 5.19.0 to 5.20.0 [Renan GEHAN]\n- Bump @babel/plugin-proposal-class-properties from 7.4.0 to 7.5.5 [Renan GEHAN]\n- Bump typescript from 3.6.3 to 3.6.4 [Renan GEHAN]\n- Bump prop-types from 15.6.2 to 15.7.2 [Renan GEHAN]\n- Bump react-sortable-hoc from 1.5.3 to 1.10.1 [Renan GEHAN]\n- Bump puppeteer from 1.18.1 to 2.0.0 [Renan GEHAN]\n- Bump tailwindcss from 1.0.4 to 1.1.3 [Renan GEHAN]\n- Bump @types/styled-components from 4.1.12 to 4.1.20 [Renan GEHAN]\n- [Security] Bump safer-eval from 1.3.3 to 1.3.5 [Renan GEHAN]\n- Bump @babel/plugin-proposal-decorators from 7.4.0 to 7.6.0 [Renan GEHAN]\n- Bump mobx-react from 6.1.1 to 6.1.4 [Renan GEHAN]\n- Bump @types/chrome from 0.0.81 to 0.0.91 [Renan GEHAN]\n- Bump sass from 1.17.3 to 1.22.12 [Renan GEHAN]\n- Bump ts-jest from 24.0.0 to 24.1.0 [Renan GEHAN]\n- Bump @types/classnames from 2.2.7 to 2.2.9 [Renan GEHAN]\n- Bump typescript from 3.5.3 to 3.6.3 [Renan GEHAN]\n- Bump mixin-deep from 1.3.1 to 1.3.2 [Renan GEHAN]\n- Bump lodash from 4.17.13 to 4.17.15 [Renan GEHAN]\n- Bump @babel/core from 7.4.0 to 7.6.0 [Renan GEHAN]\n- Show priority in Jira issue card [Renan GEHAN]\n- Revert \"Add renovate.json\" [Renan GEHAN]\n- Add renovate.json [Renan GEHAN]\n- Bump @types/object-hash from 1.2.0 to 1.3.0 [dependabot-preview[bot]]\n- Bump @babel/plugin-proposal-object-rest-spread from 7.4.0 to 7.5.5 [dependabot-preview[bot]]\n- Bump tslint from 5.14.0 to 5.19.0 [dependabot-preview[bot]]\n- Bump @types/lodash from 4.14.123 to 4.14.137 [dependabot-preview[bot]]\n- Bump @types/jest from 24.0.11 to 24.0.18 [Renan GEHAN]\n- Bump typescript from 3.3.4000 to 3.5.3 [Renan GEHAN]\n- Bump react from 16.8.1 to 16.9.0 [dependabot-preview[bot]]\n- Bump uuid from 3.3.2 to 3.3.3 [dependabot-preview[bot]]\n- Add bottom margin to FilterEditModal to add spacing between CTAs and bottom of screen [Renan GEHAN]\n- Add overflow auto to Modal content [Renan GEHAN]\n- Bump lodash from 4.17.10 to 4.17.13 [Renan GEHAN]\n- Do not show all items as new after editing a filter [Renan GEHAN]\n- Move update logic to the filter model [Renan GEHAN]\n- Clear filter's notifications when navigating to another filter [Renan GEHAN]\n- Generate screenshots with notifications too [Renan GEHAN]\n- Automatically select a filter on creation [Renan GEHAN]\n- Minor ui fixes on Github profile card [Renan GEHAN]\n- Let filters automatically refetch themselves in reaction to their hash changing [Renan GEHAN]\n- Move filter fetching logic inside of filter model (better encapsulation) [Renan GEHAN]\n- Stop resetting whole filter object when simply editing [Renan GEHAN]\n- Fix filter.clone so that it clones all relevant attributes of the model [Renan GEHAN]\n\n### v1.2.2\n- Update README with new screenshots [Renan GEHAN]\n- Fix text color on dark RepoCard [Renan GEHAN]\n- Add a few data-* attributes to make navigation easier with Puppeteer [Renan GEHAN]\n- Add a script for automatically taking Chrome Store formatted screenshots [Renan GEHAN]\n- Fix editorconfig again [Renan GEHAN]\n\n### v1.2.1\n- Fix color on active FilterLink in light mode [Renan GEHAN]\n- Remove IsDarkContext, directly import settingsStore instead [Renan GEHAN]\n- Stop relying on @inject, directly import stores instead [Renan GEHAN]\n- Convert FilterLink to TS [Renan GEHAN]\n- Add utils for exporting/import the configuration [Renan GEHAN]\n- Remove unused migrations file [Renan GEHAN]\n- Split filters store file in two: FilterStore and Filter model [Renan GEHAN]\n- Convert Dashboard to Typescript [Renan GEHAN]\n- Add small border to indicate which are the new items of a filter [Renan GEHAN]\n- In Filter, store all new items IDs, instead of just the count [Renan GEHAN]\n- Fix type [Renan GEHAN]\n- Fix default filter type predicate [Renan GEHAN]\n- Make filter sidebar sticky [Renan GEHAN]\n- Also fade out toasts when manually discarded [Renan GEHAN]\n- Fix toast z-index so that they are visible above modals [Renan GEHAN]\n- Add feedback toast to some actions [Renan GEHAN]\n- Add active colors to buttons [Renan GEHAN]\n- Minor manual adjustement after Tailwind update [Renan GEHAN]\n- Update all css classes to new tailwind format [Renan GEHAN]\n- Remove normalize.css [Renan GEHAN]\n- Update tailwindcss module to 1.0.4 [Renan GEHAN]\n- Fix .editorconfig *facepalm* [Renan GEHAN]\n\n### v1.2.0\n- Ensure cache invalidation is persistent across hard refreshes [Renan GEHAN]\n- Alter Filter model to compute count of new items since last refresh [Renan GEHAN]\n- Add method on provider to resolve UID for filter items [Renan GEHAN]\n- Add notification badge to FilterLink [Renan GEHAN]\n- Make initialize method on AbstractProvider, abstract [Renan GEHAN]\n- Slightly decouple FiltersStore and Filter model [Renan GEHAN]\n- Add .editorconfig [Renan GEHAN]\n- Allow console.log in tslint.json [Renan GEHAN]\n- Persist the selectedFilterId [Renan Gehan]\n\n### v1.1.1\n- Fix useNewTabPage setting not being taken into account [Renan Gehan]\n\n### v1.1.0\n- Add title to the check status [Renan Gehan]\n- Show whether a PR is conflicting with an icon [Renan Gehan]\n- Retrieve MergeableStatus from PRs (graphql) [Renan Gehan]\n\n### v1.0.0\n- Move IssueStatus enum to its own file [Renan GEHAN]\n- Default IssueStatus to Unknown when using Github REST fetcher [Renan Gehan]\n- Adapt CheckStatusIndicator to new status format [Renan Gehan]\n- Adapt github graphql fetcher to properly format status [Renan Gehan]\n- Fetch checkSuites associated to the last commit of a PR [Renan Gehan]\n- Add header on github fetcher allowing access to the Previews API [Renan Gehan]\n- Use enum for Issue status type [Renan Gehan]\n- Use international URL for Firefox [Renan GEHAN]\n- Bump handlebars from 4.1.1 to 4.1.2 [Renan GEHAN]\n- Bump safer-eval from 1.2.3 to 1.3.3 [Renan GEHAN]\n- Create migration for operators [Renan GEHAN]\n- Add more jira predicates [Renan GEHAN]\n- Allow submitting FilterEditModal by pressing Enter [Renan GEHAN]\n- Fix style when in dark mode [Renan GEHAN]\n- Adapt filtering UI to the operators [Renan GEHAN]\n- Replace `negated` flag on predicates by an `operator` key, allowing richer filters [Renan GEHAN]\n- Add testing utils for testing migrations in the browser [Renan GEHAN]\n- Add logging in migrations [Renan GEHAN]\n- Run migrations on app start [Renan GEHAN]\n- Create migration from v1 to v2, with tests [Renan GEHAN]\n- Disable ts diagnostics in tests [Renan GEHAN]\n- Make ts-jest work with js files too [Renan GEHAN]\n- Create migration from v0 to v1, with tests [Renan GEHAN]\n- Install jest-localstorage-mock [Renan GEHAN]\n- setup jest [Renan GEHAN]\n- Run tslint autofix on the whole project [Renan GEHAN]\n- Fix tslint setup [Renan GEHAN]\n- Upgrade build system dependencies [Renan GEHAN]\n- Improve header styling (+ ts rewrite) [Renan GEHAN]\n- Add link to the repo [Renan GEHAN]\n- Rename octolenses-browser-extension to octolenses (to match new repo url) [Renan GEHAN]\n- Cache jira filters too [Renan GEHAN]\n- Change jira service url, change jira app client id [Renan GEHAN]\n- Cache trending repos response [Renan GEHAN]\n- Flush expired cache entries on start [Renan GEHAN]\n- Create settings tab for the cache (with a clear button) [Renan GEHAN]\n- Cache jira available resources for 5m [Renan GEHAN]\n- Create filters in loading state by default [Renan GEHAN]\n- Cache results from the GitHub GraphQL endpoint [Renan GEHAN]\n- Cache results from the GitHub REST endpoint [Renan GEHAN]\n- Add a way to bypass the cache for filter refresh [Renan GEHAN]\n- Compute a hash for each filter, taking into account all data that's relevant to fetching [Renan GEHAN]\n- Implement a cache class (very similar to laravel Cache facade) [Renan GEHAN]\n- Very simple implementation of a caching service worker [Renan GEHAN]\n- Very simple implementation of a caching service worker [Renan GEHAN]\n- Fetch jira resources on init [Renan GEHAN]\n- Properly serialize predicates that can contain whitespaces [Renan GEHAN]\n- Use first jira resource by default [Renan GEHAN]\n- Only refresh jira token if it's expired, or going to in less than 5m [Renan GEHAN]\n- Add key to manifest for a stable dev url [Renan GEHAN]\n- Refresh token on init (+ add types) [Renan GEHAN]\n- Improve Modal animation [Renan GEHAN]\n- Fetch resources on jira connection [Renan GEHAN]\n- Extract token swap logic outside of the component [Renan GEHAN]\n- Use proper token swap service URL [Renan GEHAN]\n- Show jira logout button if the user's connected + it's list of available resources [Renan GEHAN]\n- Make github ProfileCard react on token change [Renan GEHAN]\n- Display user profile in the Github settings panel if the user's logged in [Renan GEHAN]\n- Minor UI fix in settings modal sidebar [Renan GEHAN]\n- Fix settings pane that are not from provides [Renan GEHAN]\n- On init, fetch Github profile [Renan GEHAN]\n- Add a way for providers to run an initialization method on start [Renan GEHAN]\n- Pass whole provider to the settings views [Renan GEHAN]\n- Create method for fetching github profile [Renan GEHAN]\n- Reorganize github fetchers [Renan GEHAN]\n- Ensure providers are persisted/hydrated just like regular stores [Renan GEHAN]\n- Adapt jira provider to the settings changes [Renan GEHAN]\n- Adapt github provider to the settings changes [Renan GEHAN]\n- Stop passing settings from the generic settings store to the provider settings views [Renan GEHAN]\n- Remove providerSettings from the settings store [Renan GEHAN]\n- Add an observable/persistable settings object on each provider, so that they manage their own settings [Renan GEHAN]\n- Transform Provider interface to an AbstractProvider abstract class [Renan GEHAN]\n- Simplify providers by extracting behavior to other files [Renan GEHAN]\n- Temporarily use a working site id [Renan GEHAN]\n- Get token at the proper setting key in jira provider [Renan GEHAN]\n- Update FilterEditModal to accomodate providers system + new UI [Renan GEHAN]\n- Rewrite SettingsModal to dynamically render registered providers settings pane [Renan GEHAN]\n- Create Jira provider [Renan GEHAN]\n- Move GitHub specific rendering logic to the provider, connect it to the settings so it can fetch with credentials [Renan GEHAN]\n- Transform Button component to TS [Renan GEHAN]\n- Create generic animated modal to be used for all modals [Renan GEHAN]\n- Add additional fonts/sizes to tailwind config [Renan GEHAN]\n- Install mobx-react-lite for use with hooks, and a few types [Renan GEHAN]\n- Rewrite Root App component in TS and provide isDark context from it [Renan GEHAN]\n- Create React context for dark mode [Renan GEHAN]\n- Add permission to use 'identity' API [Renan GEHAN]\n- Add some missing types [Renan GEHAN]\n- Move migrations to a file, make them run earlier. [Renan GEHAN]\n- Remove now useless filters lib [Renan GEHAN]\n- Fix filter addition/cloning [Renan GEHAN]\n- Decouple app from GitHub by introducing a system of providers (+ typescript rewrite) [Renan GEHAN]\n- Proper TypeScript/TSlint setup (also install lib types) [Renan GEHAN]\n- Fix comments count total computation [Renan GEHAN]\n- Show sum of reviews/comments count on the IssueCard [Renan GEHAN]\n- Add comments count (and fake reviews count) to the result of the REST query [Renan GEHAN]\n- Fetch comments/reviews totalCount in graphql query [Renan GEHAN]\n- Fix icon used in IssueCard when using REST endpoint [Renan GEHAN]\n- Disable HMR as it makes us hit rate limit really fast in dev [Renan GEHAN]\n- Fix issue status icon color when using REST endpoint [Renan Gehan]\n- Properly serialize REST filter payload [Renan Gehan]\n- Renaming CIStatusIndicator component to CheckStatusIndicator. [Renan GEHAN]\n- Adding a new merged status predicate with merged and unmerged values. [Renan GEHAN]\n- Renaming STATE_COLORS hash map to ISSUE_STATUS_COLORS. Delete unsued import package. [Renan GEHAN]\n- Extraction of issue status indicator logic in a specific IssueStatusIndicator component. [Renan GEHAN]\n- Rename StatusIndicator component to CIStatusIndicator. [Renan GEHAN]\n- Add merged and unmerged values on status filter. [Renan GEHAN]\n- Fix small layout issue [Renan GEHAN]\n- Fetch issues using the REST api when no token is set [Renan GEHAN]\n- Add initial migration for setting schemaVersion to 1 [Renan GEHAN]\n- Add schemaVersion attribute to settings store [Renan GEHAN]\n- Fix loader dark color [Renan GEHAN]\n- Add cursor:pointer on ContextDropdown trigger [Renan GEHAN]\n- 49/use-graphql-api (#57) [GitHub]\n\n### v0.4.3\n- Adapt ContextualDropdown to theme [Renan Gehan]\n- Use ContextualDropdown in IssueCard [Renan Gehan]\n- Implement ContextualDropdown [Renan Gehan]\n- Install clipboard dependency [Renan Gehan]\n- Upgrade to React 16.8 [Renan Gehan]\n\n### v0.4.2\n- Fix FilterLink observer [Renan GEHAN]\n- Prevent dragging from too far [Renan GEHAN]\n- Fix filter selection [Renan GEHAN]\n- Select the moved filter [Renan GEHAN]\n- Make filters reorderable [Renan GEHAN]\n- Update font-awesome to 5.6 [Renan GEHAN]\n- Install react-sortable-hoc [Renan GEHAN]\n- Allow removing toast by clicking on it [Renan GEHAN]\n- Fix toast colors [Renan GEHAN]\n- Show proper error when getting rate-limited [Renan GEHAN]\n- To rebase toast [Renan GEHAN]\n- Add global ToastManager to the tree [Renan GEHAN]\n- Create ToastManager for handling notifications [Renan GEHAN]\n- Ignore bundled zips [Renan GEHAN]\n\n### v0.4.1\n- Fix FilterLink count badge size [Renan GEHAN]\n- Add browser icons in the README.md to guide the user to the link [Renan GEHAN]\n- Update screenshots & README.md [Renan GEHAN]\n- Change default filter [Renan GEHAN]\n\n### v0.4.0\n- Fix image placehold bg color to avoid weird border in dark mode [Renan GEHAN]\n- Fix dark mode so that it automatically toggles at night [Renan GEHAN]\n- Fix font size [Renan GEHAN]\n- Reskin FilterLink & Button components [Renan GEHAN]\n- Fix alignment of different pages [Renan GEHAN]\n- Darken modal backdrop in dark mode [Renan GEHAN]\n- Minor UI tweaks in the Settings modal [Renan GEHAN]\n- Adapt FilterPredicate to dark mode [Renan GEHAN]\n- Reskin FilterEditModal [Renan GEHAN]\n- Reskin settings modal [Renan GEHAN]\n- Reskin dropdown [Renan GEHAN]\n- Add dark mode support in reskined components [Renan GEHAN]\n- Reskin app completely [Renan GEHAN]\n- Setup tailwindcss [Renan GEHAN]\n- Update compress script to add version to filename [Renan GEHAN]\n\n### v0.3.0\n- Showcase dark mode in the README [Renan GEHAN]\n- Improve Loader style in dark mode [Renan GEHAN]\n- Update yarn.lock with integrity entries [Renan GEHAN]\n- Improve layout of the settings modal [Renan GEHAN]\n- Add night time detection to toggle dark mode [Renan GEHAN]\n- Add a dark theme to all relevant components [Renan GEHAN]\n- Add a dark mode option [Renan GEHAN]\n\n### v0.2.3\n- Add ellipsis and overflow to repo cards titles (#46) [Renan GEHAN]\n- Add margins on Loader & error messages (#43) [GitHub]\n- Add refresh filter button (#41) [GitHub]\n- Add 'Last 2 Weeks' option to discover date ranges [Renan GEHAN]\n- Fix Dashboard container height when there are few cards [Renan GEHAN]\n\n### v0.2.2\n- Add icons on the browser_action so it works on FF [Renan GEHAN]\n\n### v0.2.1\n- Create util method for checking if url is a new tab url [Renan GEHAN]\n- Change new tab url check for firefox (#38) [Renan GEHAN]\n\n### v0.2.0\n- Update README to explain why the 'tabs' permission is required [Renan GEHAN]\n- Update README to explain how to disable the New Tab override [Renan GEHAN]\n- Allow setting the new tab behavior in the SettingsModal [Renan GEHAN]\n- Add a browser_action for opening OctoLenses [Renan GEHAN]\n- Reorganize background scripts to expose a single entry point [Renan GEHAN]\n- Update browser_action title [Renan GEHAN]\n- Let the decision of overriding the new tab page to a smart background script [Renan GEHAN]\n- Add icons [Renan GEHAN]\n- Change github personal access token creation link to prefill needed scope (#34) [Renan GEHAN]\n- Add link to the Firefox version [Renan GEHAN]\n- Improve README.md build instructions [Renan GEHAN]\n\n### v0.1.5\n- Refresh filters/trends when a token is set (fix #31) [Renan GEHAN]\n- Fix FilterEditModal title input width [Renan GEHAN]\n- Remove unnecessary permissions [Renan GEHAN]\n- Fix new filter default id not being applied [Renan GEHAN]\n- Ensure a failed filter error is removed once refetched successfully [Renan GEHAN]\n- Fix typos in README (#24) [Renan GEHAN]\n\n### v0.1.4\n- Add clone filter button (fix #16) [Renan GEHAN]\n- Improve filter actions UI in the Dashboard [Renan GEHAN]\n- Add default 'is:open' predicate to new filters [Renan GEHAN]\n- Ensure there is a filter selected after hydration [Renan GEHAN]\n- Use repo id as a key to prevent duplicate key warning when using repo name [Renan GEHAN]\n- Remove unused import [Renan GEHAN]\n- Fix FilterLink not reacting to loading/error state changes [Renan GEHAN]\n- Persist filters definitions, not their data [Renan GEHAN]\n- Persist settings and navigation store [Renan GEHAN]\n- Install mobx-persist & reorganize store bootstrap logic [Renan GEHAN]\n- Replace all rematch models by mobx stores [Renan GEHAN]\n- Fix order of babel plugins so that mobx decorators work as expected [Renan GEHAN]\n- Replace rematch with mobx [Renan GEHAN]\n\n### v0.1.3\n- Update README to reflect changes in release process [Renan GEHAN]\n- Create release script (fix #22) [Renan GEHAN]\n- Add placeholders to all predicates [Renan GEHAN]\n- Increase input predicate min-width to 250px [Renan GEHAN]\n- Add all review related filter predicates [Renan GEHAN]\n\n"
  },
  {
    "path": "cypress/e2e/discover.spec.js",
    "content": "context('Discover', () => {\n  beforeEach(() => {\n    cy.injectGithubToken();\n\n    cy.visit(Cypress.env('BASE_URL') + '/');\n\n    cy.get('[data-header-link=discover]').click();\n  });\n\n  it('loads a default set of repos', () => {\n    cy.get('[data-id=loader]');\n    cy.get('[data-id=repo-card]');\n  });\n\n  it('can change the language', () => {\n    // Let it load\n    cy.get('[data-id=loader]');\n    cy.get('[data-id=repo-card]');\n\n    // Change the language to JavaScript\n    cy.get('[data-id=dropdown-language').select('JavaScript');\n\n    // Let it load again, and check it loaded JavaScript repos\n    cy.get('[data-id=loader]');\n    cy.get('[data-id=repo-card]').contains('JavaScript');\n  });\n\n  it('can change the period', () => {\n    // Let it load\n    cy.get('[data-id=loader]');\n    cy.get('[data-id=repo-card]');\n\n    // Change the language to JavaScript\n    cy.get('[data-id=dropdown-dateRange').select('Last month');\n\n    // Let it load again, and check it loaded JavaScript repos\n    cy.get('[data-id=loader]');\n    cy.get('[data-id=repo-card]');\n  });\n\n  it('redirects to the repo on click', () => {\n    // Let it load, and assert it would have opened in a new tab on click\n    cy.get('[data-id=loader]');\n    cy.get('[data-id=repo-card]:first-child [data-id=repo-link]').should(\n      'have.attr',\n      'target',\n      '_blank'\n    );\n  });\n});\n"
  },
  {
    "path": "cypress/e2e/filters.spec.js",
    "content": "const DEFAULT_FILTER_NAME = 'OctoLenses Issues';\n\ncontext('Filters', () => {\n  beforeEach(() => {\n    cy.injectGithubToken();\n    cy.visit(Cypress.env('BASE_URL') + '/');\n  });\n\n  it('sees a default filter', () => {\n    cy.contains(DEFAULT_FILTER_NAME);\n  });\n\n  it('loads a list of issues from the default filter', () => {\n    cy.contains('rgehan/octolenses');\n  });\n\n  it('can add a GitHub filter', () => {\n    createFilter({\n      name: 'React PRs',\n      predicates: [\n        { name: 'Repository', type: 'repo', value: 'facebook/react' },\n        { name: 'Type', type: 'type', value: 'PRs', isDropdown: true },\n        { name: 'Status', type: 'status', value: 'Open', isDropdown: true },\n      ],\n    });\n\n    cy.get('[data-id=loader]');\n    cy.contains('React PRs');\n  });\n\n  it('can delete a filter', () => {\n    createFilter({\n      name: 'Laravel stuff',\n      predicates: [\n        { name: 'Repository', type: 'repo', value: 'laravel/framework' },\n      ],\n    });\n\n    cy.contains(DEFAULT_FILTER_NAME).click();\n    cy.contains('Delete').click();\n    cy.contains(DEFAULT_FILTER_NAME).should('not.exist');\n  });\n\n  it('can edit a filter', () => {\n    cy.contains(DEFAULT_FILTER_NAME).click();\n    cy.contains('Edit').click();\n\n    // Rename the filter\n    cy.get('[data-id=filter-label-input]')\n      .type('{selectAll}{del}')\n      .type('Laravel PRs');\n\n    // Change the target repository\n    cy.get(`[data-id=predicate-repo] [data-id=predicate-value-selector]`)\n      .type('{selectAll}{del}')\n      .type('laravel/framework');\n\n    // Save\n    cy.contains('Continue').click();\n\n    // Check the edited filter is in the sidebar\n    cy.get('[data-id=filter-links]').contains('Laravel PRs');\n\n    // Check we have cards corresponding to the new filter\n    cy.get('[data-id=filter-results]').contains('laravel/framework');\n  });\n\n  it('can clone a filter', () => {\n    cy.contains(DEFAULT_FILTER_NAME).click();\n    cy.contains('Clone').click();\n\n    cy.get('[data-id=filter-links]').contains(DEFAULT_FILTER_NAME + ' (Copy)');\n  });\n\n  it('can refresh a filter', () => {\n    cy.contains(DEFAULT_FILTER_NAME).click();\n\n    // Waits for results to be displayed\n    cy.get('[data-id=filter-results]').contains('rgehan/octolenses');\n\n    // Refresh\n    cy.contains('Refresh').click();\n\n    // Wait for a loader to appear\n    cy.get('[data-id=loader]');\n\n    // Check we have results again\n    cy.get('[data-id=filter-results]').contains('rgehan/octolenses');\n  });\n});\n\nfunction createFilter({ name, predicates }) {\n  // Open the filter modal\n  cy.contains('Add').click();\n\n  // Select a GitHub filter\n  cy.contains('GitHub').click();\n  cy.contains('Continue').click();\n\n  // Rename the filter\n  cy.get('[data-id=filter-label-input]')\n    .type('{selectAll}{del}')\n    .type(name);\n\n  predicates.forEach(({ name, type, value, isDropdown = false }) => {\n    cy.get('[data-id=add-predicate-dropdown]').select(name);\n\n    if (isDropdown) {\n      cy.get(\n        `[data-id=predicate-${type}] [data-id=predicate-value-selector]`\n      ).select(value);\n    } else {\n      cy.get(\n        `[data-id=predicate-${type}] [data-id=predicate-value-selector]`\n      ).type(value);\n    }\n  });\n\n  // Save the filter\n  cy.contains('Continue').click();\n}\n"
  },
  {
    "path": "cypress/e2e/settings.spec.js",
    "content": "context('Discover', () => {\n  beforeEach(() => {\n    cy.visit(Cypress.env('BASE_URL') + '/');\n\n    cy.get('[data-header-link=settings]').click();\n  });\n\n  it('opens, then closes the settings modal', () => {\n    cy.contains('Settings');\n    cy.contains('Close').click();\n    cy.contains('Settings').should('not.exist');\n  });\n\n  it('changes to night mode', () => {\n    // Check it's not using dark mode\n    cy.get('body.dark').should('not.exist');\n\n    // Switch to dark mode\n    cy.contains('Night mode').click();\n    cy.contains('Always').click();\n\n    // Check dark mode is applied\n    cy.get('body.dark');\n  });\n\n  it('clears the cache', () => {\n    cy.contains('Cache').click();\n    cy.contains('Clear cache').click();\n    cy.contains('Cache was successfully cleared');\n  });\n\n  it('sets the GitHub token', () => {\n    cy.contains('GitHub').click();\n    cy.get('input').type('my-token');\n    cy.contains('Save').click();\n    cy.contains('Token was saved');\n  });\n});\n"
  },
  {
    "path": "cypress/support/commands.js",
    "content": "Cypress.Commands.add('injectGithubToken', () => {\n  cy.window().then(window => {\n    const token = Cypress.env('GITHUB_TOKEN');\n\n    if (!token) {\n      throw new Error(\n        'No CYPRESS_GITHUB_TOKEN environement variable was provided!'\n      );\n    }\n\n    window.localStorage.setItem(\n      'githubProvider',\n      JSON.stringify({\n        settings: {\n          token,\n        },\n      })\n    );\n  });\n});\n"
  },
  {
    "path": "cypress/support/e2e.js",
    "content": "import './commands';\n"
  },
  {
    "path": "cypress.config.ts",
    "content": "import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  projectId: 'r2fbf6',\n  watchForFileChanges: false,\n  env: {\n    BASE_URL: 'http://localhost:1234',\n  },\n  fixturesFolder: false,\n  e2e: {\n    setupNodeEvents(on, config) {},\n    specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',\n  },\n})\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  preset: 'ts-jest/presets/js-with-ts',\n  testMatch: ['<rootDir>/src/**/*.test.ts'],\n  testEnvironment: 'jsdom',\n  setupFiles: ['./src/setupTests.ts'],\n  globals: {\n    'ts-jest': {\n      diagnostics: false,\n    },\n  },\n};\n"
  },
  {
    "path": "manifest.json",
    "content": "{\n  \"key\": \"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq/S9dl/VwNTcp+T5ZLnOH8gOZoMkFTiKNsR6EDv3zUiEImaheCDMzFUhRiyxg1NY55RjB7oMcqFl2RQnMr8YIR4zmCN0VKljgzN4G81mH+tN+lSccKYOPcUed26nzzzSdxryUSg2QfadChtnCfFMaZxkwrQTn4So5pTyr9upPNDvL4BCXG1sxeZHpp5TDIJTNYlKngQzmWGd/yi0QBRwukeobk7nqiXzAoXkG2qHP3A+Y3YrGFsXfB3X1rM/1g8qnUHwYlv/MmLboOmRQdEBsKdstUmmGTQb6FuTAYVkTcaw/wtRHyd+Cdf9jTo2SVdU160zZ3Ngqr7B+cYvEx+htQIDAQAB\",\n  \"manifest_version\": 3,\n  \"name\": \"OctoLenses\",\n  \"version\": \"<version>\",\n  \"short_name\": \"OctoLenses Browser Extension\",\n  \"description\": \"Watch your repos and discover awesome things directly from your New Tab page.\",\n  \"permissions\": [\"identity\"],\n  \"host_permissions\": [\n    \"*://*.github.com/*\",\n    \"https://use.fontawesome.com/*\",\n    \"https://fonts.googleapis.com/*\"\n  ],\n  \"chrome_url_overrides\": {\n    \"newtab\": \"index.html\"\n  },\n  \"background\": {\n    \"service_worker\": \"service_worker/index.js\"\n  },\n  \"action\": {\n    \"default_title\": \"OctoLenses\",\n    \"default_icon\": {\n      \"16\": \"icons/icon-16.png\",\n      \"32\": \"icons/icon-32.png\",\n      \"48\": \"icons/icon-48.png\",\n      \"128\": \"icons/icon-128.png\"\n    }\n  },\n  \"icons\": {\n    \"16\": \"icons/icon-16.png\",\n    \"32\": \"icons/icon-32.png\",\n    \"48\": \"icons/icon-48.png\",\n    \"128\": \"icons/icon-128.png\"\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"github-trending-chrome-extension\",\n  \"targets\": {\n    \"app\": {\n      \"context\": \"browser\",\n      \"distDir\": \"dist/\",\n      \"source\": \"src/index.html\"\n    },\n    \"service_worker\": {\n      \"context\": \"service-worker\",\n      \"distDir\": \"dist/service_worker/\",\n      \"source\": \"src/service_worker/index.js\"\n    }\n  },\n  \"browserslist\": \"> 0.5%, last 2 versions, not dead\",\n  \"version\": \"2.3.0\",\n  \"author\": \"Renan GEHAN <rgehan94@gmail.com>\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@babel/polyfill\": \"^7.8.3\",\n    \"@types/object-hash\": \"^1.3.0\",\n    \"babel-polyfill\": \"^6.26.0\",\n    \"classnames\": \"^2.2.6\",\n    \"clipboard\": \"^2.0.4\",\n    \"contrast\": \"^1.0.1\",\n    \"es6-error\": \"^4.1.1\",\n    \"human-format\": \"^0.10.1\",\n    \"immer\": \"^5.0.0\",\n    \"lodash\": \"^4.17.15\",\n    \"mobx\": \"^5.15.1\",\n    \"mobx-persist\": \"^0.4.1\",\n    \"mobx-react\": \"^6.1.4\",\n    \"moment\": \"^2.22.2\",\n    \"object-hash\": \"^2.0.1\",\n    \"prop-types\": \"^15.7.2\",\n    \"react\": \"^16.12.0\",\n    \"react-dom\": \"^16.8.0\",\n    \"react-sortable-hoc\": \"^1.10.1\",\n    \"recompose\": \"^0.30.0\",\n    \"styled-components\": \"^4.1.3\",\n    \"timeago.js\": \"^4.0.1\",\n    \"uuid\": \"^3.3.3\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.8.7\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.7.0\",\n    \"@babel/plugin-proposal-decorators\": \"^7.6.0\",\n    \"@babel/plugin-proposal-object-rest-spread\": \"^7.8.3\",\n    \"@parcel/config-default\": \"^2.10.0\",\n    \"@parcel/transformer-sass\": \"2.10.0\",\n    \"@parcel/transformer-typescript-tsc\": \"^2.10.0\",\n    \"@types/chrome\": \"^0.0.239\",\n    \"@types/classnames\": \"^2.2.9\",\n    \"@types/clipboard\": \"^2.0.1\",\n    \"@types/jest\": \"^24.0.22\",\n    \"@types/lodash\": \"^4.14.146\",\n    \"@types/node\": \"^20.3.3\",\n    \"@types/react-dom\": \"^16.9.4\",\n    \"@types/react-sortable-hoc\": \"^0.7.1\",\n    \"@types/recompose\": \"^0.30.7\",\n    \"@types/styled-components\": \"^4.1.20\",\n    \"@types/uuid\": \"^3.4.4\",\n    \"@typescript-eslint/eslint-plugin\": \"^2.8.0\",\n    \"@typescript-eslint/parser\": \"^2.19.2\",\n    \"autoprefixer\": \"^10.4.16\",\n    \"cypress\": \"12.16.0\",\n    \"eslint\": \"^6.7.2\",\n    \"eslint-plugin-react\": \"^7.18.3\",\n    \"jest\": \"^24.5.0\",\n    \"jest-localstorage-mock\": \"^2.4.0\",\n    \"parcel\": \"^2.0.0\",\n    \"prettier\": \"^1.19.1\",\n    \"process\": \"^0.11.10\",\n    \"puppeteer\": \"^2.0.0\",\n    \"rimraf\": \"^3.0.2\",\n    \"sass\": \"^1.24.2\",\n    \"tailwindcss\": \"2\",\n    \"ts-jest\": \"^24.3.0\",\n    \"typescript\": \"^3.8.3\"\n  },\n  \"scripts\": {\n    \"clean\": \"rimraf dist/ .parcel-cache\",\n    \"start\": \"parcel serve --no-hmr --no-autoinstall src/index.html\",\n    \"copyManifest\": \"cp manifest.json dist/manifest.json\",\n    \"copyIcons\": \"cp -r icons/ dist/icons/\",\n    \"syncManifestVersion\": \"version=$(jq -r .version package.json) && sed -i '' \\\"s/<version>/$version/\\\" dist/manifest.json && echo \\\"Updated manifest with version: $version\\\"\",\n    \"build:unpacked\": \"yarn clean && parcel build && yarn copyManifest && yarn copyIcons && yarn syncManifestVersion\",\n    \"compress\": \"cd dist/ && zip -rq ../octolenses-$(jq -r .version ../package.json).zip *\",\n    \"build\": \"yarn build:unpacked && yarn compress\",\n    \"release\": \"./scripts/release\",\n    \"screenshots\": \"./scripts/screenshots\",\n    \"test\": \"jest\",\n    \"e2e\": \"cypress open\",\n    \"lint\": \"eslint --ext ts,tsx src/\"\n  }\n}"
  },
  {
    "path": "postcss.config.js",
    "content": "const tailwindcss = require('tailwindcss');\n\n// prettier-ignore\nmodule.exports = {\n  plugins: [\n    tailwindcss(),\n    require('autoprefixer'),\n  ],\n};\n"
  },
  {
    "path": "scripts/release",
    "content": "#!/usr/bin/env node\n\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst { readFileSync, writeFileSync } = require('fs');\nconst { includes, isBuffer } = require('lodash');\nconst semver = require('semver');\n\nconst RELEASE_TYPES = ['patch', 'minor', 'major'];\n\nconst exec = command => {\n  const stdout = execSync(command);\n  return isBuffer(stdout) ? stdout.toString() : stdout;\n};\n\n// Ensure the release type is valid\nconst releaseType = process.argv[2];\nif (!includes(RELEASE_TYPES, releaseType)) {\n  console.log(`Usage: yarn release ${RELEASE_TYPES.join('|')}`);\n  process.exit(1);\n}\n\n// Compute the new version number\nconst packageJsonPath = path.resolve(__dirname, '../package.json');\nconst packageJson = require(packageJsonPath);\nconst currentVersion = packageJson.version;\nconst newVersion = semver.inc(currentVersion, releaseType);\nconsole.log(`1. Incrementing version from ${currentVersion} to ${newVersion}`);\n\n// Generate and write the changelog\nconsole.log('2. Writing changelog...');\nconst changelogPath = path.resolve(__dirname, '../changelog.md');\nconst changelogAdditions = exec(`git log v${currentVersion}..HEAD --pretty=\"- %s [%cn]\"`);\nconst existingChangelog = readFileSync(changelogPath);\nconst newChangelog = `### v${newVersion}\\n${changelogAdditions}\\n${existingChangelog}`;\nwriteFileSync(changelogPath, newChangelog);\n\n// Update package.json version & create the release commit\nconsole.log('3. Updating package.json version number...');\npackageJson.version = newVersion;\nwriteFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));\n\nconsole.log('4. Creating release commit...')\nexec(`git add changelog.md package.json`);\nexec(`git commit -m \"v${newVersion}\"`);\n\nconsole.log('5. Creating release tag...');\nexec(`git tag v${newVersion}`);\n\nconsole.log('[INFO] To publish the changes, run:');\nconsole.log('$ git push origin HEAD && git push --tags');\n"
  },
  {
    "path": "scripts/screenshots",
    "content": "#!/usr/bin/env node\n\nconst path = require('path');\n\nconst Bundler = require('parcel-bundler');\nconst puppeteer = require('puppeteer');\nconst { kebabCase } = require('lodash');\n\n(async () => {\n  const port = await startDevelopmentServer();\n  const { browser, page } = await setupPuppeteer(port);\n  await takeScreenshots(page);\n\n  console.log('All done.');\n\n  browser.close();\n  process.exit(0);\n})();\n\nasync function startDevelopmentServer() {\n  console.log('Starting development server...');\n\n  const bundler = new Bundler(path.resolve(__dirname, '../src/index.html'), {\n    hmr: false,\n  });\n\n  const server = await bundler.serve();\n\n  console.log('Started.');\n\n  return server.address().port;\n}\n\nasync function setupPuppeteer(port) {\n  const browser = await puppeteer.launch({\n    // headless: false,\n    // devtools: false,\n  });\n\n  const page = await browser.newPage();\n\n  // Setup the viewport\n  await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });\n\n  await page.goto(`http://localhost:${port}`);\n\n  return { browser, page };\n}\n\nasync function takeScreenshots(page) {\n  // Move to the app, wait for it to load\n\n  const DARK_MODES = ['DISABLED', 'ENABLED'];\n  const SCENARIOS = [\n    screenshotDashboard,\n    screenshotDiscover,\n    screenshotFilterEditModal,\n    screenshotFilterAddModal,\n    screenshotSettings,\n  ];\n\n  for (darkMode of DARK_MODES) {\n    console.log(`[darkMode: ${darkMode}]`);\n\n    await page.evaluate(darkMode => {\n      window.stores.settingsStore.darkMode = darkMode;\n    }, darkMode);\n\n    const screenshotFolder = path.resolve(\n      __dirname,\n      '../.github/screenshots/',\n      darkMode === 'ENABLED' ? 'dark' : 'light'\n    );\n\n    for (scenario of SCENARIOS) {\n      await scenario(page, screenshotFolder);\n    }\n  }\n}\n\nasync function screenshotDashboard(page, screenshotFolder) {\n  console.log('Capturing screenshot of the Dashboard page...');\n\n  await page.waitForFunction(() => !document.querySelector('[data-id=loader]'));\n\n  await page.waitFor(300);\n\n  await page.evaluate(() => {\n    const filter = window.stores.filtersStore.data[0];\n    const identifiers = filter.data.slice(0, 2).map(filter => filter.number);\n    filter.newItemsIdentifiers = identifiers;\n  });\n\n  await page.screenshot({\n    path: path.resolve(screenshotFolder, './dashboard.png'),\n  });\n}\n\nasync function screenshotDiscover(page, screenshotFolder) {\n  console.log('Capturing screenshot of the Discover page...');\n\n  await page.click('[data-header-link=discover]');\n  await page.waitForFunction(() => !document.querySelector('[data-id=loader]'));\n\n  await page.screenshot({\n    path: path.resolve(screenshotFolder, './discover.png'),\n  });\n\n  await page.click('[data-header-link=dashboard]');\n}\n\nasync function screenshotFilterEditModal(page, screenshotFolder) {\n  console.log('Capturing screenshot of the filter edition modal...');\n\n  await page.click('.fa-edit');\n  await page.waitFor(500);\n\n  await page.screenshot({\n    path: path.resolve(screenshotFolder, './filter-edit.png'),\n  });\n\n  await page.keyboard.press('Escape');\n}\n\nasync function screenshotFilterAddModal(page, screenshotFolder) {\n  console.log('Capturing screenshot of the filter addition modal...');\n\n  await page.click('.fa-plus-square');\n  await page.waitFor(500);\n\n  await page.screenshot({\n    path: path.resolve(screenshotFolder, './filter-add.png'),\n  });\n\n  await page.keyboard.press('Escape');\n}\n\nasync function screenshotSettings(page, screenshotFolder) {\n  console.log('Capturing screenshot of the night mode settings modal...');\n\n  // Open the settings modal\n  await page.click('.fa-cog');\n  await page.waitFor(300);\n\n  // Screenshot all tabs\n  const SETTING_TABS = ['Night mode', 'Cache', 'GitHub', 'Jira'];\n\n  for (tab of SETTING_TABS) {\n    await page.click(`[data-setting-tab=\"${tab}\"]`);\n\n    await page.screenshot({\n      path: path.resolve(screenshotFolder, `./settings-${kebabCase(tab)}.png`),\n    });\n  }\n\n  await page.keyboard.press('Escape');\n}\n"
  },
  {
    "path": "src/@types/contrast/index.d.ts",
    "content": "declare module 'contrast';\n"
  },
  {
    "path": "src/@types/human-format/index.d.ts",
    "content": "declare module 'human-format';\n"
  },
  {
    "path": "src/App.tsx",
    "content": "import { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport { Header, ToastManager } from './components';\nimport { Dashboard, Discover } from './pages';\nimport { NavigationStore } from './store/navigation';\n\nconst PAGES = {\n  discover: Discover,\n  dashboard: Dashboard,\n};\n\ntype PageName = keyof typeof PAGES;\n\ninterface IInnerProps {\n  navigationStore: NavigationStore;\n}\n\nexport const App = compose<IInnerProps, {}>(\n  inject('navigationStore'),\n  observer\n)(({ navigationStore }) => {\n  const Page = PAGES[navigationStore.page as PageName];\n\n  return (\n    <div className=\"App\">\n      <Header />\n      <Page />\n      <ToastManager />\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/components/Button/Button.tsx",
    "content": "import cx from 'classnames';\nimport React, { ReactNode } from 'react';\n\nexport enum ButtonType {\n  PRIMARY = 'primary',\n  DEFAULT = 'default',\n}\n\nconst TYPE_TO_CLASSES: Record<ButtonType, string> = {\n  primary: 'bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white',\n  default: 'bg-gray-200 hover:bg-gray-400 active:bg-gray-500 text-black',\n};\n\ninterface IProps {\n  type?: ButtonType;\n  onClick: () => void;\n  children: ReactNode;\n  className?: string;\n}\n\nexport const Button = ({\n  type = ButtonType.DEFAULT,\n  onClick,\n  children,\n  className = '',\n}: IProps) => (\n  <button\n    className={cx(\n      'min-w-24 px-3 py-2 rounded cursor-pointer',\n      'outline-none hover:outline-none focus:outline-none active:outline-none',\n      TYPE_TO_CLASSES[type],\n      className\n    )}\n    onClick={onClick}\n  >\n    {children}\n  </button>\n);\n"
  },
  {
    "path": "src/components/Button/index.ts",
    "content": "export { Button, ButtonType } from './Button';\n"
  },
  {
    "path": "src/components/Dropdown/Dropdown.tsx",
    "content": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React, { ChangeEvent } from 'react';\nimport { compose } from 'recompose';\n\nimport { SettingsStore } from '../../store/settings';\n\ninterface IProps {\n  value: string;\n  name: string;\n  items: IOption[];\n  onChange: (option: IOption) => void;\n  className?: string;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\ninterface IOption {\n  value: string;\n  name: string;\n}\n\nexport const Dropdown = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ name, items, value, className, settingsStore, onChange }) => {\n  function handleChange(event: ChangeEvent<HTMLSelectElement>) {\n    onChange({ name, value: event.target.value });\n  }\n\n  return (\n    <div\n      className={cx(\n        'w-48 flex relative rounded shadow-lg',\n        settingsStore.isDark ? 'bg-gray-800 text-white' : 'bg-white',\n        className\n      )}\n    >\n      <select\n        onChange={handleChange}\n        value={value}\n        className={cx(\n          'flex-1 border-none bg-transparent cursor-pointer outline-none appearance-none py-2 px-3',\n          settingsStore.isDark && 'text-white'\n        )}\n        data-id={`dropdown-${name}`}\n      >\n        {items.map(item => (\n          <option key={item.value} value={item.value}>\n            {item.name}\n          </option>\n        ))}\n      </select>\n      <i className=\"fa fa-caret-down absolute right-0 mt-2 mr-3\" />\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/components/Dropdown/index.ts",
    "content": "export { Dropdown } from './Dropdown';\n"
  },
  {
    "path": "src/components/FilterLink/FilterLink.tsx",
    "content": "import cx from 'classnames';\nimport { size } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport {\n  SortableElement,\n  SortableHandle,\n  SortableElementProps,\n} from 'react-sortable-hoc';\nimport { compose } from 'recompose';\n\nimport { Filter } from '../../store/models/filter';\nimport { SettingsStore } from '../../store/settings';\nimport { Loader } from '../Loader';\n\nconst DragHandle = SortableHandle(\n  observer(({ isDark }: { isDark: boolean }) => (\n    <i\n      className={cx(\n        'fas fa-grip-horizontal text-sm mr-1',\n        isDark ? 'text-gray-700' : 'text-gray-500'\n      )}\n    />\n  ))\n);\n\ninterface IProps extends SortableElementProps {\n  filter: Filter;\n  isSelected: boolean;\n  onClick: () => void;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const FilterLink = compose<IInnerProps, IProps>(\n  SortableElement,\n  inject('settingsStore'),\n  observer\n)(({ filter, isSelected, onClick, settingsStore }) => {\n  const { loading, error } = filter;\n  const activeColor = settingsStore.isDark ? 'text-gray-500' : 'text-gray-900';\n\n  return (\n    <div\n      key={filter.id}\n      className={cx(\n        'flex items-center text-right rtl pr-4 mb-3 cursor-pointer whitespace-nowrap select-none',\n        `hover:${activeColor}`,\n        isSelected ? activeColor : 'text-gray-600'\n      )}\n      onClick={onClick}\n    >\n      <span\n        className={cx(\n          'rounded-full flex-shrink-0 flex items-center justify-center text-xs h-4 w-8 ml-2 relative',\n          settingsStore.isDark ? 'bg-gray-800' : 'bg-gray-400'\n        )}\n      >\n        {loading && <Loader size={13} strokeWidth={12} />}\n        {!loading && error && <i className=\"fa fa-times\" />}\n        {!loading && !error && size(filter.data)}\n        {filter.newItemsCount > 0 && !filter.loading && (\n          <div className=\"absolute right-0 top-0 w-4 h-4 -mr-2 -mt-2 bg-red-600 text-white rounded-full flex items-center justify-center\">\n            {filter.newItemsCount <= 99 ? (\n              <span className=\"text-2xs\">{filter.newItemsCount}</span>\n            ) : (\n              <span className=\"text-base\">•</span>\n            )}\n          </div>\n        )}\n      </span>\n      <bdi>{filter.label}</bdi>\n      <DragHandle isDark={settingsStore.isDark} />\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/components/FilterLink/index.ts",
    "content": "export { FilterLink } from './FilterLink';\n"
  },
  {
    "path": "src/components/FilterPredicate/FilterPredicate.tsx",
    "content": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\nimport styled from 'styled-components';\n\nimport { AbstractProvider } from '../../providers';\n\nimport { SettingsStore } from '../../store/settings';\nimport { OperatorSelector } from './OperatorSelector';\nimport { ValueSelector } from './ValueSelector';\n\nconst Wrapper = styled.div`\n  .action-icon {\n    display: none;\n  }\n\n  :hover {\n    .action-icon {\n      display: initial;\n    }\n  }\n`;\n\ninterface IProps {\n  type: string;\n  operator: string;\n  value: string;\n  provider: AbstractProvider;\n  onChange: (payload: object) => void;\n  onDelete: () => void;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const FilterPredicate = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ type, operator, value, provider, onDelete, onChange, settingsStore }) => {\n  const predicate = provider.findPredicate(type);\n\n  const handleChange = (key: string) => (newValue: string) =>\n    onChange({\n      type,\n      operator,\n      value,\n      [key]: newValue,\n    });\n\n  return (\n    <Wrapper className=\"flex relative mb-3\" data-id={`predicate-${type}`}>\n      <div\n        className={cx(\n          'flex-1 flex items-stretch rounded overflow-hidden text-lg',\n          settingsStore.isDark ? 'bg-gray-800' : 'bg-gray-200'\n        )}\n      >\n        <div\n          className={cx(\n            'text-white py-2 px-3',\n            settingsStore.isDark ? 'bg-gray-700' : 'bg-gray-600'\n          )}\n        >\n          <span>{predicate.label}</span>\n          <OperatorSelector\n            predicate={predicate}\n            value={operator}\n            onChange={handleChange('operator')}\n          />\n        </div>\n        <div className=\"flex-1 flex\">\n          <ValueSelector\n            predicate={predicate}\n            value={value}\n            onChange={handleChange('value')}\n          />\n        </div>\n      </div>\n      <div\n        className=\"absolute inset-y-0 flex items-center px-4\"\n        style={{ left: '100%' }}\n      >\n        <i\n          className={cx(\n            'action-icon far fa-trash-alt cursor-pointer text-gray-600',\n            settingsStore.isDark ? 'hover:text-gray-500' : 'hover:text-gray-800'\n          )}\n          onClick={onDelete}\n        />\n      </div>\n    </Wrapper>\n  );\n});\n"
  },
  {
    "path": "src/components/FilterPredicate/OperatorSelector.tsx",
    "content": "import React from 'react';\n\nimport { Predicate } from '../../providers';\n\ninterface IProps {\n  predicate: Predicate;\n  value: string;\n  onChange: (value: string) => void;\n}\n\nexport const OperatorSelector = ({ predicate, value, onChange }: IProps) => {\n  if (predicate.operators.length === 0) {\n    return null;\n  }\n\n  return (\n    <select\n      className=\"ml-2 outline-none text-black\"\n      value={value}\n      onChange={event => onChange(event.target.value)}\n      data-id=\"operator-dropdown\"\n    >\n      {predicate.operators.map(item => (\n        <option key={item.value} value={item.value}>\n          {item.label}\n        </option>\n      ))}\n    </select>\n  );\n};\n"
  },
  {
    "path": "src/components/FilterPredicate/ValueSelector.tsx",
    "content": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport { Predicate, PredicateType } from '../../providers';\nimport { SettingsStore } from '../../store/settings';\n\ninterface IProps {\n  predicate: Predicate;\n  value: string;\n  onChange: (value: string) => void;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const ValueSelector = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ predicate, value, onChange, settingsStore }) => {\n  const baseStyle = cx(\n    'h-full flex-1 bg-transparent outline-none',\n    settingsStore.isDark ? 'text-white' : 'text-gray-800'\n  );\n\n  if (predicate.type === PredicateType.TEXT) {\n    return (\n      <input\n        type=\"text\"\n        value={value}\n        onChange={event => onChange(event.target.value)}\n        placeholder={predicate.placeholder}\n        className={cx(baseStyle, 'pl-3')}\n        data-id=\"predicate-value-selector\"\n      />\n    );\n  }\n\n  if (predicate.type === PredicateType.DROPDOWN) {\n    return (\n      <select\n        value={value}\n        onChange={event => onChange(event.target.value)}\n        className={cx(baseStyle, 'ml-2 mr-3')}\n        data-id=\"predicate-value-selector\"\n      >\n        <option key=\"__default\" value=\"\">\n          Choose...\n        </option>\n        {predicate.choices.map(choice => (\n          <option key={choice.value} value={choice.value}>\n            {choice.label}\n          </option>\n        ))}\n      </select>\n    );\n  }\n\n  return null;\n});\n"
  },
  {
    "path": "src/components/FilterPredicate/index.ts",
    "content": "export { FilterPredicate } from './FilterPredicate';\n"
  },
  {
    "path": "src/components/Header/Header.tsx",
    "content": "import cx from 'classnames';\nimport { capitalize } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport React, { useState } from 'react';\nimport { compose } from 'recompose';\n\nimport { SettingsModal } from '../../containers';\nimport { NavigationStore } from '../../store/navigation';\nimport { SettingsStore } from '../../store/settings';\nimport { TabLink } from './TabLink';\n\ninterface IInnerProps {\n  settingsStore: SettingsStore;\n  navigationStore: NavigationStore;\n}\n\nexport const Header = compose<IInnerProps, {}>(\n  inject('settingsStore', 'navigationStore'),\n  observer\n)(({ settingsStore, navigationStore }) => {\n  const [modalOpen, setModalOpen] = useState(false);\n\n  function renderLink(name: string) {\n    const { page, navigateTo } = navigationStore;\n    return (\n      <TabLink\n        onClick={() => navigateTo(name)}\n        active={page === name}\n        name={name}\n      >\n        {capitalize(name)}\n      </TabLink>\n    );\n  }\n\n  return (\n    <React.Fragment>\n      <div className=\"h-24 flex flex-col Header\">\n        <div className=\"flex items-center\">\n          <a\n            href=\"https://github.com/rgehan/octolenses\"\n            className={cx(\n              'font-roboto text-4xl font-bold mt-4',\n              settingsStore.isDark ? 'text-white' : 'text-gray-900'\n            )}\n          >\n            <i className=\"fab fa-github mr-3\" />\n            <span>OctoLenses</span>\n          </a>\n          <div className=\"tabs flex-1 flex justify-end\">\n            {renderLink('dashboard')}\n            {renderLink('discover')}\n            <TabLink onClick={() => setModalOpen(true)} name=\"settings\">\n              <i className=\"fa fa-cog\" />\n            </TabLink>\n          </div>\n        </div>\n      </div>\n      {modalOpen && <SettingsModal onClose={() => setModalOpen(false)} />}\n    </React.Fragment>\n  );\n});\n"
  },
  {
    "path": "src/components/Header/TabLink.tsx",
    "content": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport { SettingsStore } from '../../store/settings';\n\nconst COLORS = {\n  dark: {\n    active: 'text-white',\n    inactive: 'text-gray-400 hover:text-white',\n  },\n  light: {\n    active: 'text-gray-800',\n    inactive: 'text-gray-600 hover:text-gray-800',\n  },\n};\n\ninterface IProps {\n  onClick: () => void;\n  name: string;\n  active?: boolean;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const TabLink = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ children, name, onClick, active = false, settingsStore }) => (\n  <a\n    className={cx(\n      'font-roboto ml-4 py-2 cursor-pointer',\n      COLORS[settingsStore.isDark ? 'dark' : 'light'][\n        active ? 'active' : 'inactive'\n      ]\n    )}\n    onClick={onClick}\n    data-header-link={name}\n  >\n    {children}\n  </a>\n));\n"
  },
  {
    "path": "src/components/Header/index.ts",
    "content": "export { Header } from './Header';\n"
  },
  {
    "path": "src/components/Loader/Loader.tsx",
    "content": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport { SettingsStore } from '../../store/settings';\n\ninterface IProps {\n  size?: number;\n  strokeWidth?: number;\n  className?: string;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const Loader = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ size = 50, strokeWidth = 10, className, settingsStore }) => (\n  <div\n    data-id=\"loader\"\n    className={cx('flex items-center justify-center h-full w-full', className)}\n  >\n    <svg\n      width={`${size}px`}\n      height={`${size}px`}\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 100 100\"\n      preserveAspectRatio=\"xMidYMid\"\n      className=\"lds-rolling\"\n      style={{ background: 'none' }}\n    >\n      <circle\n        cx=\"50\"\n        cy=\"50\"\n        fill=\"none\"\n        stroke={settingsStore.isDark ? '#606f7b' : '#678bc2'}\n        strokeWidth={strokeWidth}\n        r=\"35\"\n        strokeDasharray=\"164.93361431346415 56.97787143782138\"\n        transform=\"rotate(41.8255 50 50)\"\n      >\n        <animateTransform\n          attributeName=\"transform\"\n          type=\"rotate\"\n          calcMode=\"linear\"\n          values=\"0 50 50;360 50 50\"\n          keyTimes=\"0;1\"\n          dur=\"1s\"\n          begin=\"0s\"\n          repeatCount=\"indefinite\"\n        />\n      </circle>\n    </svg>\n  </div>\n));\n"
  },
  {
    "path": "src/components/Loader/index.ts",
    "content": "export { Loader } from './Loader';\n"
  },
  {
    "path": "src/components/Modal/Modal.tsx",
    "content": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React, { ReactNode, useEffect } from 'react';\nimport { compose } from 'recompose';\nimport styled, { keyframes } from 'styled-components';\n\nimport { SettingsStore } from '../../store/settings';\n\nconst fadeIn = keyframes`\n  from { opacity: 0; }\n  to { opacity: 1; }\n`;\n\nconst Backdrop = styled.div`\n  animation: ${fadeIn} 0.25s ease;\n`;\n\nconst slideBottom = keyframes`\n  from { transform: translateY(100%); }\n  to { transform: translateY(0); }\n`;\n\nconst Wrapper = styled.div`\n  animation: ${slideBottom} 0.25s ease;\n`;\n\ninterface IProps {\n  children: ReactNode;\n  onClose: () => void;\n  className?: string;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const Modal = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ children, onClose, settingsStore }) => {\n  // Close the modal on ESC\n  useEffect(() => {\n    function handleKeyDown(event: KeyboardEvent) {\n      if (event.key === 'Escape') {\n        onClose();\n      }\n    }\n\n    window.addEventListener('keydown', handleKeyDown);\n    return () => window.removeEventListener('keydown', handleKeyDown);\n  }, [onClose]);\n\n  return (\n    <Backdrop\n      className={cx(\n        'fixed z-50 inset-0 font-roboto text-lg',\n        settingsStore.isDark ? 'bg-gray-900' : 'bg-white'\n      )}\n    >\n      <div\n        onClick={onClose}\n        className={cx(\n          'flex items-center absolute top-0 right-0 mt-4 mr-4 cursor-pointer py-1 px-2 rounded-full',\n          settingsStore.isDark\n            ? 'text-gray-500 hover:bg-gray-800'\n            : 'text-gray-700 hover:bg-gray-200'\n        )}\n      >\n        <span className=\"mr-2\">Close</span>\n        <i className=\"fa fa-times\" />\n      </div>\n      <Wrapper className=\"h-full overflow-auto\">{children}</Wrapper>\n    </Backdrop>\n  );\n});\n"
  },
  {
    "path": "src/components/Modal/index.ts",
    "content": "export { Modal } from './Modal';\n"
  },
  {
    "path": "src/components/RadioCard/RadioCard.tsx",
    "content": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport { SettingsStore } from '../../store/settings';\n\ninterface IProps {\n  title: string;\n  text: string;\n  selected: boolean;\n  icon?: string;\n  dark?: boolean;\n  onClick: () => void;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nconst COLORS = {\n  dark: {\n    active: 'bg-gray-800 border-blue-500',\n    inactive: 'bg-gray-800 border-gray-800',\n  },\n  light: {\n    active: 'border-blue-500 bg-blue-100 text-blue-800',\n    inactive: 'border-gray bg-gray-100 text-gray-800',\n  },\n};\n\nexport const RadioCard = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ title, text, selected, icon, onClick, settingsStore }) => (\n  <div\n    onClick={onClick}\n    className={cx(\n      'h-28 flex border px-3 py-2 rounded cursor-pointer select-none mb-3',\n      COLORS[settingsStore.isDark ? 'dark' : 'light'][\n        selected ? 'active' : 'inactive'\n      ]\n    )}\n  >\n    <div className=\"pr-2 pt-px\">\n      <input type=\"radio\" checked={selected} />\n    </div>\n    <div className=\"leading-normal flex-1\">\n      <div className=\"font-medium\">{title}</div>\n      <div className=\"mt-1\">{text}</div>\n    </div>\n    {icon && (\n      <div className=\"w-20 flex items-center justify-center ml-2\">\n        <i\n          className={cx(\n            'text-5xl',\n            selected ? 'text-blue-500' : 'text-gray-400',\n            icon\n          )}\n        />\n      </div>\n    )}\n  </div>\n));\n"
  },
  {
    "path": "src/components/RadioCard/index.ts",
    "content": "export { RadioCard } from './RadioCard';\n"
  },
  {
    "path": "src/components/ToastManager/Toast.tsx",
    "content": "import cx from 'classnames';\nimport React from 'react';\nimport styled from 'styled-components';\n\nimport { INotification } from './types';\n\nconst TYPES_TO_ICON = {\n  info: 'fa-info-circle',\n  error: 'fa-exclamation-circle',\n};\n\nconst TYPES_TO_THEME = {\n  info: 'bg-blue-500 text-white',\n  error: 'bg-red-400 text-white',\n};\n\nconst TOAST_DURATION = 3000;\nconst TOAST_FADE_DURATION = 200;\n\nexport const ToastTypes = Object.keys(TYPES_TO_ICON);\n\nconst Wrapper = styled.div`\n  transition: opacity 0.2s;\n`;\n\ninterface IProps extends INotification {\n  onRemove: (id: string) => void;\n}\n\nexport class Toast extends React.Component<IProps> {\n  public state = {\n    visible: true,\n  };\n\n  public componentDidMount() {\n    setTimeout(this.discardToast, TOAST_DURATION);\n  }\n\n  public discardToast = () => {\n    this.setState({ visible: false });\n    setTimeout(this.removeToast, TOAST_FADE_DURATION);\n  };\n\n  public removeToast = () => {\n    const { id, onRemove } = this.props;\n    onRemove(id);\n  };\n\n  public render() {\n    const { message, type } = this.props;\n    const { visible } = this.state;\n\n    return (\n      <Wrapper\n        onClick={this.discardToast}\n        className={cx(\n          'bg-gray-800 px-4 py-3 rounded shadow-md mt-3 select-none pointer-events-auto cursor-pointer',\n          TYPES_TO_THEME[type],\n          !visible && 'opacity-0'\n        )}\n      >\n        <i className={cx('fas mr-2', TYPES_TO_ICON[type])} />\n        <span>{message}</span>\n      </Wrapper>\n    );\n  }\n}\n"
  },
  {
    "path": "src/components/ToastManager/ToastManager.tsx",
    "content": "import { reject } from 'lodash';\nimport React from 'react';\n\nimport { Toast } from './Toast';\nimport { INotification, NotificationType } from './types';\n\n// This will contain a ToastManager instance reference so that toasts\n// can be created from anywhere without having to store toasts state\n// inside MobX or having to do weird findDOMNode stuff.\nlet manager: ToastManager = null;\n\nconst generateRandomId = () => Math.floor(Math.random() * 0x10000).toString(16);\n\ninterface IState {\n  notifications: INotification[];\n}\n\nexport class ToastManager extends React.Component<{}, IState> {\n  public state: IState = {\n    notifications: [],\n  };\n\n  public componentDidMount() {\n    manager = this;\n  }\n\n  public addNotification(message: string, type: NotificationType) {\n    const { notifications } = this.state;\n\n    const notification = {\n      id: generateRandomId(),\n      message,\n      type,\n    };\n\n    this.setState({\n      notifications: [notification, ...notifications],\n    });\n  }\n\n  public onRemoveNotification = (id: string) => {\n    this.setState({\n      notifications: reject(this.state.notifications, { id }),\n    });\n  };\n\n  public render() {\n    const { notifications } = this.state;\n    return (\n      <div className=\"z-50 fixed top-0 inset-x-0 flex flex-col items-center pointer-events-none\">\n        {notifications.map(notification => (\n          <Toast\n            key={notification.id}\n            onRemove={this.onRemoveNotification}\n            {...notification}\n          />\n        ))}\n      </div>\n    );\n  }\n}\n\n/**\n * Syntax sugar allowing to safely inject a toast inside the ToastManager singleton\n * @param {*} message\n * @param {*} type\n */\nexport function toast(message: string, type: NotificationType) {\n  if (!manager) {\n    return;\n  }\n\n  manager.addNotification(message, type);\n}\n"
  },
  {
    "path": "src/components/ToastManager/index.ts",
    "content": "export { ToastManager, toast } from './ToastManager';\n"
  },
  {
    "path": "src/components/ToastManager/types.ts",
    "content": "export interface INotification {\n  id: string;\n  message: string;\n  type: NotificationType;\n}\n\nexport type NotificationType = 'info' | 'error';\n"
  },
  {
    "path": "src/components/index.ts",
    "content": "export { Dropdown } from './Dropdown';\nexport { Loader } from './Loader';\nexport { Header } from './Header';\nexport { FilterLink } from './FilterLink';\nexport { ToastManager } from './ToastManager';\n"
  },
  {
    "path": "src/constants/darkMode.ts",
    "content": "export const DARK_MODE = {\n  DISABLED: 'DISABLED',\n  ENABLED: 'ENABLED',\n  AT_NIGHT: 'AT_NIGHT',\n};\n"
  },
  {
    "path": "src/constants/dates.ts",
    "content": "import { find } from 'lodash';\nimport moment from 'moment';\n\nexport type DateType =\n  | 'last_week'\n  | 'last_two_weeks'\n  | 'last_month'\n  | 'last_six_months'\n  | 'last_year'\n  | 'anytime';\n\ninterface IDatePreset {\n  name: string;\n  value: DateType;\n  data: {\n    amount: any;\n    unit: any;\n  };\n}\n\nexport const DATES: IDatePreset[] = [\n  { name: 'Last week', value: 'last_week', data: { amount: 1, unit: 'week' } },\n  {\n    name: 'Last 2 weeks',\n    value: 'last_two_weeks',\n    data: { amount: 2, unit: 'weeks' },\n  },\n  {\n    name: 'Last month',\n    value: 'last_month',\n    data: { amount: 1, unit: 'month' },\n  },\n  {\n    name: 'Last 6 months',\n    value: 'last_six_months',\n    data: { amount: 6, unit: 'months' },\n  },\n  { name: 'Last year', value: 'last_year', data: { amount: 1, unit: 'year' } },\n  { name: 'Anytime', value: 'anytime', data: { amount: 10, unit: 'years' } },\n];\n\n/**\n * Returns a formatted date as used in a Github filter from\n * the value of the date object\n */\nexport const getDateFromValue = (value: DateType) => {\n  const {\n    data: { amount, unit },\n  } = find(DATES, { value });\n\n  return moment()\n    .subtract(amount, unit)\n    .format('YYYY-MM-DD');\n};\n"
  },
  {
    "path": "src/constants/languages.ts",
    "content": "export const LANGUAGES = [\n  { value: null, name: 'All Languages' },\n  { value: '1c-enterprise', name: '1C Enterprise' },\n  { value: 'abap', name: 'ABAP' },\n  { value: 'abnf', name: 'ABNF' },\n  { value: 'actionscript', name: 'ActionScript' },\n  { value: 'ada', name: 'Ada' },\n  { value: 'adobe-font-metrics', name: 'Adobe Font Metrics' },\n  { value: 'agda', name: 'Agda' },\n  { value: 'ags-script', name: 'AGS Script' },\n  { value: 'alloy', name: 'Alloy' },\n  { value: 'alpine-abuild', name: 'Alpine Abuild' },\n  { value: 'ampl', name: 'AMPL' },\n  { value: 'angelscript', name: 'AngelScript' },\n  { value: 'ant-build-system', name: 'Ant Build System' },\n  { value: 'antlr', name: 'ANTLR' },\n  { value: 'apacheconf', name: 'ApacheConf' },\n  { value: 'apex', name: 'Apex' },\n  { value: 'api-blueprint', name: 'API Blueprint' },\n  { value: 'apl', name: 'APL' },\n  { value: 'apollo-guidance-computer', name: 'Apollo Guidance Computer' },\n  { value: 'applescript', name: 'AppleScript' },\n  { value: 'arc', name: 'Arc' },\n  { value: 'asciidoc', name: 'AsciiDoc' },\n  { value: 'asn.1', name: 'ASN.1' },\n  { value: 'asp', name: 'ASP' },\n  { value: 'aspectj', name: 'AspectJ' },\n  { value: 'assembly', name: 'Assembly' },\n  { value: 'ats', name: 'ATS' },\n  { value: 'augeas', name: 'Augeas' },\n  { value: 'autohotkey', name: 'AutoHotkey' },\n  { value: 'autoit', name: 'AutoIt' },\n  { value: 'awk', name: 'Awk' },\n  { value: 'ballerina', name: 'Ballerina' },\n  { value: 'batchfile', name: 'Batchfile' },\n  { value: 'befunge', name: 'Befunge' },\n  { value: 'bison', name: 'Bison' },\n  { value: 'bitbake', name: 'BitBake' },\n  { value: 'blade', name: 'Blade' },\n  { value: 'blitzbasic', name: 'BlitzBasic' },\n  { value: 'blitzmax', name: 'BlitzMax' },\n  { value: 'bluespec', name: 'Bluespec' },\n  { value: 'boo', name: 'Boo' },\n  { value: 'brainfuck', name: 'Brainfuck' },\n  { value: 'brightscript', name: 'Brightscript' },\n  { value: 'bro', name: 'Bro' },\n  { value: 'c', name: 'C' },\n  { value: 'c%23', name: 'C#' },\n  { value: 'c++', name: 'C++' },\n  { value: 'c-objdump', name: 'C-ObjDump' },\n  { value: 'c2hs-haskell', name: 'C2hs Haskell' },\n  { value: \"cap'n-proto\", name: \"Cap'n Proto\" },\n  { value: 'cartocss', name: 'CartoCSS' },\n  { value: 'ceylon', name: 'Ceylon' },\n  { value: 'chapel', name: 'Chapel' },\n  { value: 'charity', name: 'Charity' },\n  { value: 'chuck', name: 'ChucK' },\n  { value: 'cirru', name: 'Cirru' },\n  { value: 'clarion', name: 'Clarion' },\n  { value: 'clean', name: 'Clean' },\n  { value: 'click', name: 'Click' },\n  { value: 'clips', name: 'CLIPS' },\n  { value: 'clojure', name: 'Clojure' },\n  { value: 'closure-templates', name: 'Closure Templates' },\n  {\n    value: 'cloud-firestore-security-rules',\n    name: 'Cloud Firestore Security Rules',\n  },\n  { value: 'cmake', name: 'CMake' },\n  { value: 'cobol', name: 'COBOL' },\n  { value: 'coffeescript', name: 'CoffeeScript' },\n  { value: 'coldfusion', name: 'ColdFusion' },\n  { value: 'coldfusion-cfc', name: 'ColdFusion CFC' },\n  { value: 'collada', name: 'COLLADA' },\n  { value: 'common-lisp', name: 'Common Lisp' },\n  { value: 'common-workflow-language', name: 'Common Workflow Language' },\n  { value: 'component-pascal', name: 'Component Pascal' },\n  { value: 'conll-u', name: 'CoNLL-U' },\n  { value: 'cool', name: 'Cool' },\n  { value: 'coq', name: 'Coq' },\n  { value: 'cpp-objdump', name: 'Cpp-ObjDump' },\n  { value: 'creole', name: 'Creole' },\n  { value: 'crystal', name: 'Crystal' },\n  { value: 'cson', name: 'CSON' },\n  { value: 'csound', name: 'Csound' },\n  { value: 'csound-document', name: 'Csound Document' },\n  { value: 'csound-score', name: 'Csound Score' },\n  { value: 'css', name: 'CSS' },\n  { value: 'csv', name: 'CSV' },\n  { value: 'cuda', name: 'Cuda' },\n  { value: 'cweb', name: 'CWeb' },\n  { value: 'cycript', name: 'Cycript' },\n  { value: 'cython', name: 'Cython' },\n  { value: 'd', name: 'D' },\n  { value: 'd-objdump', name: 'D-ObjDump' },\n  { value: 'darcs-patch', name: 'Darcs Patch' },\n  { value: 'dart', name: 'Dart' },\n  { value: 'dataweave', name: 'DataWeave' },\n  { value: 'desktop', name: 'desktop' },\n  { value: 'diff', name: 'Diff' },\n  { value: 'digital-command-language', name: 'DIGITAL Command Language' },\n  { value: 'dm', name: 'DM' },\n  { value: 'dns-zone', name: 'DNS Zone' },\n  { value: 'dockerfile', name: 'Dockerfile' },\n  { value: 'dogescript', name: 'Dogescript' },\n  { value: 'dtrace', name: 'DTrace' },\n  { value: 'dylan', name: 'Dylan' },\n  { value: 'e', name: 'E' },\n  { value: 'eagle', name: 'Eagle' },\n  { value: 'easybuild', name: 'Easybuild' },\n  { value: 'ebnf', name: 'EBNF' },\n  { value: 'ec', name: 'eC' },\n  { value: 'ecere-projects', name: 'Ecere Projects' },\n  { value: 'ecl', name: 'ECL' },\n  { value: 'eclipse', name: 'ECLiPSe' },\n  { value: 'edje-data-collection', name: 'Edje Data Collection' },\n  { value: 'edn', name: 'edn' },\n  { value: 'eiffel', name: 'Eiffel' },\n  { value: 'ejs', name: 'EJS' },\n  { value: 'elixir', name: 'Elixir' },\n  { value: 'elm', name: 'Elm' },\n  { value: 'emacs-lisp', name: 'Emacs Lisp' },\n  { value: 'emberscript', name: 'EmberScript' },\n  { value: 'eq', name: 'EQ' },\n  { value: 'erlang', name: 'Erlang' },\n  { value: 'f%23', name: 'F#' },\n  { value: 'factor', name: 'Factor' },\n  { value: 'fancy', name: 'Fancy' },\n  { value: 'fantom', name: 'Fantom' },\n  { value: 'filebench-wml', name: 'Filebench WML' },\n  { value: 'filterscript', name: 'Filterscript' },\n  { value: 'fish', name: 'fish' },\n  { value: 'flux', name: 'FLUX' },\n  { value: 'formatted', name: 'Formatted' },\n  { value: 'forth', name: 'Forth' },\n  { value: 'fortran', name: 'Fortran' },\n  { value: 'freemarker', name: 'FreeMarker' },\n  { value: 'frege', name: 'Frege' },\n  { value: 'g-code', name: 'G-code' },\n  { value: 'game-maker-language', name: 'Game Maker Language' },\n  { value: 'gams', name: 'GAMS' },\n  { value: 'gap', name: 'GAP' },\n  { value: 'gcc-machine-description', name: 'GCC Machine Description' },\n  { value: 'gdb', name: 'GDB' },\n  { value: 'gdscript', name: 'GDScript' },\n  { value: 'genie', name: 'Genie' },\n  { value: 'genshi', name: 'Genshi' },\n  { value: 'gentoo-ebuild', name: 'Gentoo Ebuild' },\n  { value: 'gentoo-eclass', name: 'Gentoo Eclass' },\n  { value: 'gerber-image', name: 'Gerber Image' },\n  { value: 'gettext-catalog', name: 'Gettext Catalog' },\n  { value: 'gherkin', name: 'Gherkin' },\n  { value: 'glsl', name: 'GLSL' },\n  { value: 'glyph', name: 'Glyph' },\n  { value: 'gn', name: 'GN' },\n  { value: 'gnuplot', name: 'Gnuplot' },\n  { value: 'go', name: 'Go' },\n  { value: 'golo', name: 'Golo' },\n  { value: 'gosu', name: 'Gosu' },\n  { value: 'grace', name: 'Grace' },\n  { value: 'gradle', name: 'Gradle' },\n  { value: 'grammatical-framework', name: 'Grammatical Framework' },\n  { value: 'graph-modeling-language', name: 'Graph Modeling Language' },\n  { value: 'graphql', name: 'GraphQL' },\n  { value: 'graphviz-(dot)', name: 'Graphviz (DOT)' },\n  { value: 'groovy', name: 'Groovy' },\n  { value: 'groovy-server-pages', name: 'Groovy Server Pages' },\n  { value: 'hack', name: 'Hack' },\n  { value: 'haml', name: 'Haml' },\n  { value: 'handlebars', name: 'Handlebars' },\n  { value: 'harbour', name: 'Harbour' },\n  { value: 'haskell', name: 'Haskell' },\n  { value: 'haxe', name: 'Haxe' },\n  { value: 'hcl', name: 'HCL' },\n  { value: 'hiveql', name: 'HiveQL' },\n  { value: 'hlsl', name: 'HLSL' },\n  { value: 'html', name: 'HTML' },\n  { value: 'html+django', name: 'HTML+Django' },\n  { value: 'html+ecr', name: 'HTML+ECR' },\n  { value: 'html+eex', name: 'HTML+EEX' },\n  { value: 'html+erb', name: 'HTML+ERB' },\n  { value: 'html+php', name: 'HTML+PHP' },\n  { value: 'http', name: 'HTTP' },\n  { value: 'hxml', name: 'HXML' },\n  { value: 'hy', name: 'Hy' },\n  { value: 'hyphy', name: 'HyPhy' },\n  { value: 'idl', name: 'IDL' },\n  { value: 'idris', name: 'Idris' },\n  { value: 'igor-pro', name: 'IGOR Pro' },\n  { value: 'inform-7', name: 'Inform 7' },\n  { value: 'ini', name: 'INI' },\n  { value: 'inno-setup', name: 'Inno Setup' },\n  { value: 'io', name: 'Io' },\n  { value: 'ioke', name: 'Ioke' },\n  { value: 'irc-log', name: 'IRC log' },\n  { value: 'isabelle', name: 'Isabelle' },\n  { value: 'isabelle-root', name: 'Isabelle ROOT' },\n  { value: 'j', name: 'J' },\n  { value: 'jasmin', name: 'Jasmin' },\n  { value: 'java', name: 'Java' },\n  { value: 'java-server-pages', name: 'Java Server Pages' },\n  { value: 'javascript', name: 'JavaScript' },\n  { value: 'jflex', name: 'JFlex' },\n  { value: 'jison', name: 'Jison' },\n  { value: 'jison-lex', name: 'Jison Lex' },\n  { value: 'jolie', name: 'Jolie' },\n  { value: 'json', name: 'JSON' },\n  { value: 'json-with-comments', name: 'JSON with Comments' },\n  { value: 'json5', name: 'JSON5' },\n  { value: 'jsoniq', name: 'JSONiq' },\n  { value: 'jsonld', name: 'JSONLD' },\n  { value: 'jsx', name: 'JSX' },\n  { value: 'julia', name: 'Julia' },\n  { value: 'jupyter-notebook', name: 'Jupyter Notebook' },\n  { value: 'kicad-layout', name: 'KiCad Layout' },\n  { value: 'kicad-legacy-layout', name: 'KiCad Legacy Layout' },\n  { value: 'kicad-schematic', name: 'KiCad Schematic' },\n  { value: 'kit', name: 'Kit' },\n  { value: 'kotlin', name: 'Kotlin' },\n  { value: 'krl', name: 'KRL' },\n  { value: 'labview', name: 'LabVIEW' },\n  { value: 'lasso', name: 'Lasso' },\n  { value: 'latte', name: 'Latte' },\n  { value: 'lean', name: 'Lean' },\n  { value: 'less', name: 'Less' },\n  { value: 'lex', name: 'Lex' },\n  { value: 'lfe', name: 'LFE' },\n  { value: 'lilypond', name: 'LilyPond' },\n  { value: 'limbo', name: 'Limbo' },\n  { value: 'linker-script', name: 'Linker Script' },\n  { value: 'linux-kernel-module', name: 'Linux Kernel Module' },\n  { value: 'liquid', name: 'Liquid' },\n  { value: 'literate-agda', name: 'Literate Agda' },\n  { value: 'literate-coffeescript', name: 'Literate CoffeeScript' },\n  { value: 'literate-haskell', name: 'Literate Haskell' },\n  { value: 'livescript', name: 'LiveScript' },\n  { value: 'llvm', name: 'LLVM' },\n  { value: 'logos', name: 'Logos' },\n  { value: 'logtalk', name: 'Logtalk' },\n  { value: 'lolcode', name: 'LOLCODE' },\n  { value: 'lookml', name: 'LookML' },\n  { value: 'loomscript', name: 'LoomScript' },\n  { value: 'lsl', name: 'LSL' },\n  { value: 'lua', name: 'Lua' },\n  { value: 'm', name: 'M' },\n  { value: 'm4', name: 'M4' },\n  { value: 'm4sugar', name: 'M4Sugar' },\n  { value: 'makefile', name: 'Makefile' },\n  { value: 'mako', name: 'Mako' },\n  { value: 'markdown', name: 'Markdown' },\n  { value: 'marko', name: 'Marko' },\n  { value: 'mask', name: 'Mask' },\n  { value: 'mathematica', name: 'Mathematica' },\n  { value: 'matlab', name: 'Matlab' },\n  { value: 'maven-pom', name: 'Maven POM' },\n  { value: 'max', name: 'Max' },\n  { value: 'maxscript', name: 'MAXScript' },\n  { value: 'mediawiki', name: 'MediaWiki' },\n  { value: 'mercury', name: 'Mercury' },\n  { value: 'meson', name: 'Meson' },\n  { value: 'metal', name: 'Metal' },\n  { value: 'minid', name: 'MiniD' },\n  { value: 'mirah', name: 'Mirah' },\n  { value: 'modelica', name: 'Modelica' },\n  { value: 'modula-2', name: 'Modula-2' },\n  { value: 'modula-3', name: 'Modula-3' },\n  { value: 'module-management-system', name: 'Module Management System' },\n  { value: 'monkey', name: 'Monkey' },\n  { value: 'moocode', name: 'Moocode' },\n  { value: 'moonscript', name: 'MoonScript' },\n  { value: 'mql4', name: 'MQL4' },\n  { value: 'mql5', name: 'MQL5' },\n  { value: 'mtml', name: 'MTML' },\n  { value: 'muf', name: 'MUF' },\n  { value: 'mupad', name: 'mupad' },\n  { value: 'myghty', name: 'Myghty' },\n  { value: 'ncl', name: 'NCL' },\n  { value: 'nearley', name: 'Nearley' },\n  { value: 'nemerle', name: 'Nemerle' },\n  { value: 'nesc', name: 'nesC' },\n  { value: 'netlinx', name: 'NetLinx' },\n  { value: 'netlinx+erb', name: 'NetLinx+ERB' },\n  { value: 'netlogo', name: 'NetLogo' },\n  { value: 'newlisp', name: 'NewLisp' },\n  { value: 'nextflow', name: 'Nextflow' },\n  { value: 'nginx', name: 'Nginx' },\n  { value: 'nim', name: 'Nim' },\n  { value: 'ninja', name: 'Ninja' },\n  { value: 'nit', name: 'Nit' },\n  { value: 'nix', name: 'Nix' },\n  { value: 'nl', name: 'NL' },\n  { value: 'nsis', name: 'NSIS' },\n  { value: 'nu', name: 'Nu' },\n  { value: 'numpy', name: 'NumPy' },\n  { value: 'objdump', name: 'ObjDump' },\n  { value: 'objective-c', name: 'Objective-C' },\n  { value: 'objective-c++', name: 'Objective-C++' },\n  { value: 'objective-j', name: 'Objective-J' },\n  { value: 'ocaml', name: 'OCaml' },\n  { value: 'omgrofl', name: 'Omgrofl' },\n  { value: 'ooc', name: 'ooc' },\n  { value: 'opa', name: 'Opa' },\n  { value: 'opal', name: 'Opal' },\n  { value: 'opencl', name: 'OpenCL' },\n  { value: 'openedge-abl', name: 'OpenEdge ABL' },\n  { value: 'openrc-runscript', name: 'OpenRC runscript' },\n  { value: 'openscad', name: 'OpenSCAD' },\n  { value: 'opentype-feature-file', name: 'OpenType Feature File' },\n  { value: 'org', name: 'Org' },\n  { value: 'ox', name: 'Ox' },\n  { value: 'oxygene', name: 'Oxygene' },\n  { value: 'oz', name: 'Oz' },\n  { value: 'p4', name: 'P4' },\n  { value: 'pan', name: 'Pan' },\n  { value: 'papyrus', name: 'Papyrus' },\n  { value: 'parrot', name: 'Parrot' },\n  { value: 'parrot-assembly', name: 'Parrot Assembly' },\n  {\n    value: 'parrot-internal-representation',\n    name: 'Parrot Internal Representation',\n  },\n  { value: 'pascal', name: 'Pascal' },\n  { value: 'pawn', name: 'PAWN' },\n  { value: 'pep8', name: 'Pep8' },\n  { value: 'perl', name: 'Perl' },\n  { value: 'perl-6', name: 'Perl 6' },\n  { value: 'php', name: 'PHP' },\n  { value: 'pic', name: 'Pic' },\n  { value: 'pickle', name: 'Pickle' },\n  { value: 'picolisp', name: 'PicoLisp' },\n  { value: 'piglatin', name: 'PigLatin' },\n  { value: 'pike', name: 'Pike' },\n  { value: 'plpgsql', name: 'PLpgSQL' },\n  { value: 'plsql', name: 'PLSQL' },\n  { value: 'pod', name: 'Pod' },\n  { value: 'pogoscript', name: 'PogoScript' },\n  { value: 'pony', name: 'Pony' },\n  { value: 'postcss', name: 'PostCSS' },\n  { value: 'postscript', name: 'PostScript' },\n  { value: 'pov-ray-sdl', name: 'POV-Ray SDL' },\n  { value: 'powerbuilder', name: 'PowerBuilder' },\n  { value: 'powershell', name: 'PowerShell' },\n  { value: 'processing', name: 'Processing' },\n  { value: 'prolog', name: 'Prolog' },\n  { value: 'propeller-spin', name: 'Propeller Spin' },\n  { value: 'protocol-buffer', name: 'Protocol Buffer' },\n  { value: 'public-key', name: 'Public Key' },\n  { value: 'pug', name: 'Pug' },\n  { value: 'puppet', name: 'Puppet' },\n  { value: 'pure-data', name: 'Pure Data' },\n  { value: 'purebasic', name: 'PureBasic' },\n  { value: 'purescript', name: 'PureScript' },\n  { value: 'python', name: 'Python' },\n  { value: 'python-console', name: 'Python console' },\n  { value: 'python-traceback', name: 'Python traceback' },\n  { value: 'q', name: 'q' },\n  { value: 'qmake', name: 'QMake' },\n  { value: 'qml', name: 'QML' },\n  { value: 'r', name: 'R' },\n  { value: 'racket', name: 'Racket' },\n  { value: 'ragel', name: 'Ragel' },\n  { value: 'raml', name: 'RAML' },\n  { value: 'rascal', name: 'Rascal' },\n  { value: 'raw-token-data', name: 'Raw token data' },\n  { value: 'rdoc', name: 'RDoc' },\n  { value: 'realbasic', name: 'REALbasic' },\n  { value: 'reason', name: 'Reason' },\n  { value: 'rebol', name: 'Rebol' },\n  { value: 'red', name: 'Red' },\n  { value: 'redcode', name: 'Redcode' },\n  { value: 'regular-expression', name: 'Regular Expression' },\n  { value: \"ren'py\", name: \"Ren'Py\" },\n  { value: 'renderscript', name: 'RenderScript' },\n  { value: 'restructuredtext', name: 'reStructuredText' },\n  { value: 'rexx', name: 'REXX' },\n  { value: 'rhtml', name: 'RHTML' },\n  { value: 'ring', name: 'Ring' },\n  { value: 'rmarkdown', name: 'RMarkdown' },\n  { value: 'robotframework', name: 'RobotFramework' },\n  { value: 'roff', name: 'Roff' },\n  { value: 'rouge', name: 'Rouge' },\n  { value: 'rpc', name: 'RPC' },\n  { value: 'rpm-spec', name: 'RPM Spec' },\n  { value: 'ruby', name: 'Ruby' },\n  { value: 'runoff', name: 'RUNOFF' },\n  { value: 'rust', name: 'Rust' },\n  { value: 'sage', name: 'Sage' },\n  { value: 'saltstack', name: 'SaltStack' },\n  { value: 'sas', name: 'SAS' },\n  { value: 'sass', name: 'Sass' },\n  { value: 'scala', name: 'Scala' },\n  { value: 'scaml', name: 'Scaml' },\n  { value: 'scheme', name: 'Scheme' },\n  { value: 'scilab', name: 'Scilab' },\n  { value: 'scss', name: 'SCSS' },\n  { value: 'sed', name: 'sed' },\n  { value: 'self', name: 'Self' },\n  { value: 'shaderlab', name: 'ShaderLab' },\n  { value: 'shell', name: 'Shell' },\n  { value: 'shellsession', name: 'ShellSession' },\n  { value: 'shen', name: 'Shen' },\n  { value: 'slash', name: 'Slash' },\n  { value: 'slim', name: 'Slim' },\n  { value: 'smali', name: 'Smali' },\n  { value: 'smalltalk', name: 'Smalltalk' },\n  { value: 'smarty', name: 'Smarty' },\n  { value: 'smt', name: 'SMT' },\n  { value: 'solidity', name: 'Solidity' },\n  { value: 'sourcepawn', name: 'SourcePawn' },\n  { value: 'sparql', name: 'SPARQL' },\n  { value: 'spline-font-database', name: 'Spline Font Database' },\n  { value: 'sqf', name: 'SQF' },\n  { value: 'sql', name: 'SQL' },\n  { value: 'sqlpl', name: 'SQLPL' },\n  { value: 'squirrel', name: 'Squirrel' },\n  { value: 'srecode-template', name: 'SRecode Template' },\n  { value: 'stan', name: 'Stan' },\n  { value: 'standard-ml', name: 'Standard ML' },\n  { value: 'stata', name: 'Stata' },\n  { value: 'ston', name: 'STON' },\n  { value: 'stylus', name: 'Stylus' },\n  { value: 'subrip-text', name: 'SubRip Text' },\n  { value: 'sugarss', name: 'SugarSS' },\n  { value: 'supercollider', name: 'SuperCollider' },\n  { value: 'svg', name: 'SVG' },\n  { value: 'swift', name: 'Swift' },\n  { value: 'systemverilog', name: 'SystemVerilog' },\n  { value: 'tcl', name: 'Tcl' },\n  { value: 'tcsh', name: 'Tcsh' },\n  { value: 'tea', name: 'Tea' },\n  { value: 'terra', name: 'Terra' },\n  { value: 'tex', name: 'TeX' },\n  { value: 'text', name: 'Text' },\n  { value: 'textile', name: 'Textile' },\n  { value: 'thrift', name: 'Thrift' },\n  { value: 'ti-program', name: 'TI Program' },\n  { value: 'tla', name: 'TLA' },\n  { value: 'toml', name: 'TOML' },\n  { value: 'turing', name: 'Turing' },\n  { value: 'turtle', name: 'Turtle' },\n  { value: 'twig', name: 'Twig' },\n  { value: 'txl', name: 'TXL' },\n  { value: 'type-language', name: 'Type Language' },\n  { value: 'typescript', name: 'TypeScript' },\n  { value: 'unified-parallel-c', name: 'Unified Parallel C' },\n  { value: 'unity3d-asset', name: 'Unity3D Asset' },\n  { value: 'unix-assembly', name: 'Unix Assembly' },\n  { value: 'uno', name: 'Uno' },\n  { value: 'unrealscript', name: 'UnrealScript' },\n  { value: 'urweb', name: 'UrWeb' },\n  { value: 'vala', name: 'Vala' },\n  { value: 'vcl', name: 'VCL' },\n  { value: 'verilog', name: 'Verilog' },\n  { value: 'vhdl', name: 'VHDL' },\n  { value: 'vim-script', name: 'Vim script' },\n  { value: 'visual-basic', name: 'Visual Basic' },\n  { value: 'volt', name: 'Volt' },\n  { value: 'vue', name: 'Vue' },\n  { value: 'wavefront-material', name: 'Wavefront Material' },\n  { value: 'wavefront-object', name: 'Wavefront Object' },\n  { value: 'wdl', name: 'wdl' },\n  { value: 'web-ontology-language', name: 'Web Ontology Language' },\n  { value: 'webassembly', name: 'WebAssembly' },\n  { value: 'webidl', name: 'WebIDL' },\n  { value: 'wisp', name: 'wisp' },\n  {\n    value: 'world-of-warcraft-addon-data',\n    name: 'World of Warcraft Addon Data',\n  },\n  { value: 'x-bitmap', name: 'X BitMap' },\n  { value: 'x-pixmap', name: 'X PixMap' },\n  { value: 'x10', name: 'X10' },\n  { value: 'xbase', name: 'xBase' },\n  { value: 'xc', name: 'XC' },\n  { value: 'xcompose', name: 'XCompose' },\n  { value: 'xml', name: 'XML' },\n  { value: 'xojo', name: 'Xojo' },\n  { value: 'xpages', name: 'XPages' },\n  { value: 'xproc', name: 'XProc' },\n  { value: 'xquery', name: 'XQuery' },\n  { value: 'xs', name: 'XS' },\n  { value: 'xslt', name: 'XSLT' },\n  { value: 'xtend', name: 'Xtend' },\n  { value: 'yacc', name: 'Yacc' },\n  { value: 'yaml', name: 'YAML' },\n  { value: 'yang', name: 'YANG' },\n  { value: 'yara', name: 'YARA' },\n  { value: 'zephir', name: 'Zephir' },\n  { value: 'zimpl', name: 'Zimpl' },\n];\n"
  },
  {
    "path": "src/containers/FilterEditModal/FilterEditModal.tsx",
    "content": "import { pick } from 'lodash';\nimport { toJS } from 'mobx';\nimport { inject, observer } from 'mobx-react';\nimport React, { useState } from 'react';\nimport { compose } from 'recompose';\nimport styled from 'styled-components';\n\nimport { Modal } from '../../components/Modal';\nimport { providers, ProviderType } from '../../providers';\nimport { Filter, FiltersStore } from '../../store/filters';\nimport { PredicatesStep } from './PredicatesStep';\nimport { ProviderStep } from './ProviderStep';\n\nconst Container = styled.div`\n  width: 650px;\n`;\n\nenum STEPS {\n  PROVIDERS,\n  PREDICATES,\n}\n\ninterface IProps {\n  initialFilter?: Filter;\n  onClose: () => void;\n}\n\ninterface IInnerProps extends IProps {\n  filtersStore: FiltersStore;\n}\n\nexport const FilterEditModal = compose<IInnerProps, IProps>(\n  inject('filtersStore'),\n  observer\n)(({ initialFilter, onClose, filtersStore }) => {\n  const [step, setStep] = useState(\n    initialFilter ? STEPS.PREDICATES : STEPS.PROVIDERS\n  );\n\n  const defaultedFilter = defaultFilter(initialFilter);\n\n  const [provider, setProvider] = useState(defaultedFilter.provider);\n  const [label, setLabel] = useState(defaultedFilter.label);\n  const [predicates, setPredicates] = useState(defaultedFilter.predicates);\n\n  function handleSave() {\n    filtersStore.saveFilter({\n      id: defaultedFilter.id,\n      provider,\n      label,\n      predicates,\n    });\n\n    onClose();\n  }\n\n  return (\n    <Modal onClose={onClose}>\n      <Container className=\"mt-32 mb-16 mx-auto\">\n        {step === STEPS.PROVIDERS && (\n          <ProviderStep\n            provider={provider}\n            onChange={setProvider}\n            previous={onClose}\n            next={() => setStep(STEPS.PREDICATES)}\n          />\n        )}\n        {step === STEPS.PREDICATES && (\n          <PredicatesStep\n            label={label}\n            setLabel={setLabel}\n            predicates={predicates}\n            setPredicates={setPredicates}\n            provider={providers[provider]}\n            previous={onClose}\n            next={handleSave}\n          />\n        )}\n      </Container>\n    </Modal>\n  );\n});\n\nfunction defaultFilter(filter?: Filter) {\n  if (!filter) {\n    return {\n      id: undefined,\n      provider: ProviderType.GITHUB,\n      label: 'Unnamed filter',\n      predicates: [],\n    };\n  }\n\n  return pick(toJS(filter), ['id', 'provider', 'label', 'predicates']);\n}\n"
  },
  {
    "path": "src/containers/FilterEditModal/PredicatesStep.tsx",
    "content": "import cx from 'classnames';\nimport { chain, get } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport React, { ChangeEvent, useEffect } from 'react';\nimport { compose } from 'recompose';\n\nimport { Button, ButtonType } from '../../components/Button';\nimport { FilterPredicate } from '../../components/FilterPredicate';\nimport { AbstractProvider, IStoredPredicate } from '../../providers';\nimport { SettingsStore } from '../../store/settings';\n\ninterface IProps {\n  label: string;\n  predicates: any[]; // TODO\n  provider: AbstractProvider;\n  setLabel: (name: string) => void;\n  setPredicates: (predicates: any[]) => void; // TODO\n  previous: () => void;\n  next: () => void;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const PredicatesStep = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(\n  ({\n    label,\n    predicates,\n    provider,\n    setLabel,\n    setPredicates,\n    previous,\n    next,\n    settingsStore,\n  }) => {\n    // Save on Enter\n    useEffect(() => {\n      function handleKeyDown(event: KeyboardEvent) {\n        if (event.key === 'Enter') {\n          next();\n        }\n      }\n\n      window.addEventListener('keydown', handleKeyDown);\n      return () => window.removeEventListener('keydown', handleKeyDown);\n    }, [next]);\n\n    /**\n     * Add a new predicate to the list of predicates\n     */\n    function handleAddPredicate(event: ChangeEvent<HTMLSelectElement>) {\n      const predicate = provider.findPredicate(event.target.value);\n\n      if (!predicate) {\n        return;\n      }\n\n      setPredicates([\n        ...predicates,\n        {\n          type: predicate.name,\n          operator: get(predicate, 'operators.0.value'),\n          value: '',\n        },\n      ]);\n    }\n\n    /**\n     * Update a predicate in place\n     * @param index Index at which the predicate is located\n     */\n    const handlePredicateChange = (index: number) => ({\n      value,\n      operator,\n    }: IStoredPredicate) => {\n      setPredicates([\n        ...predicates.slice(0, index),\n        { ...predicates[index], value, operator },\n        ...predicates.slice(index + 1),\n      ]);\n    };\n\n    /**\n     * Remove a predicate\n     * @param index Index at which the predicate is located\n     */\n    const handlePredicateDeletion = (index: number) => () => {\n      setPredicates([\n        ...predicates.slice(0, index),\n        ...predicates.slice(index + 1),\n      ]);\n    };\n\n    return (\n      <div>\n        <div className=\"text-base text-gray-500 font-medium tracking-wider\">\n          Filter name\n        </div>\n        <input\n          type=\"text\"\n          value={label}\n          className={cx(\n            'w-full text-2xl bg-transparent outline-none',\n            settingsStore.isDark ? 'text-white' : 'text-black'\n          )}\n          onChange={event => setLabel(event.target.value)}\n          data-id=\"filter-label-input\"\n        />\n        <div className=\"text-base text-gray-500 font-medium tracking-wider mt-8 mb-2\">\n          Predicates\n        </div>\n        <div>\n          {predicates.map((predicate, index) => (\n            <FilterPredicate\n              key={index}\n              {...predicate}\n              provider={provider}\n              onChange={handlePredicateChange(index)}\n              onDelete={handlePredicateDeletion(index)}\n            />\n          ))}\n        </div>\n        <div>\n          <select\n            value=\"\"\n            onChange={handleAddPredicate}\n            className={cx(\n              'bg-transparent appearance-none border-none outline-none cursor-pointer',\n              settingsStore.isDark ? 'text-gray-500' : 'text-gray-900'\n            )}\n            data-id=\"add-predicate-dropdown\"\n          >\n            <option key=\"__default\" value=\"\">\n              + Add a predicate\n            </option>\n            {chain(provider.getAvailablePredicates())\n              .orderBy('label')\n              .map(({ name, label: predicateLabel }) => (\n                <option key={name} value={name}>\n                  {predicateLabel}\n                </option>\n              ))\n              .value()}\n          </select>\n        </div>\n        <div className=\"flex justify-end mt-10\">\n          <Button onClick={previous} className=\"mr-3\">\n            Cancel\n          </Button>\n          <Button onClick={next} type={ButtonType.PRIMARY}>\n            Continue\n          </Button>\n        </div>\n      </div>\n    );\n  }\n);\n"
  },
  {
    "path": "src/containers/FilterEditModal/ProviderStep.tsx",
    "content": "import React from 'react';\n\nimport { Button, ButtonType } from '../../components/Button';\nimport { RadioCard } from '../../components/RadioCard';\nimport { ProviderType } from '../../providers';\n\ninterface IProps {\n  provider: ProviderType;\n  onChange: (provider: ProviderType) => void;\n  previous: () => void;\n  next: () => void;\n}\n\nexport const ProviderStep = ({\n  provider,\n  onChange,\n  previous,\n  next,\n}: IProps) => {\n  return (\n    <div className=\"flex-1 flex flex-col items-stretch\">\n      <div className=\"font-medium mb-4\">\n        Where do you want to get data from?\n      </div>\n      <RadioCard\n        title=\"GitHub\"\n        text=\"Filter issues and pull requests from public and private repositories.\"\n        icon=\"fab fa-github\"\n        selected={provider === ProviderType.GITHUB}\n        onClick={() => onChange(ProviderType.GITHUB)}\n      />\n      <RadioCard\n        title=\"Jira\"\n        text=\"Filter and retrieve tickets from any Jira organization you may have access to.\"\n        icon=\"fab fa-jira\"\n        selected={provider === ProviderType.JIRA}\n        onClick={() => onChange(ProviderType.JIRA)}\n      />\n      <div className=\"flex justify-end mt-10\">\n        <Button onClick={previous} className=\"mr-3\">\n          Cancel\n        </Button>\n        <Button onClick={next} type={ButtonType.PRIMARY}>\n          Continue\n        </Button>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/containers/FilterEditModal/index.ts",
    "content": "export { FilterEditModal } from './FilterEditModal';\n"
  },
  {
    "path": "src/containers/RepoCard/RepoCard.tsx",
    "content": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport cx from 'classnames';\nimport humanFormat from 'human-format';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport { SettingsStore } from '../../store/settings';\n\ninterface IProps {\n  repo: any;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const RepoCard = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ repo, settingsStore }) => {\n  const {\n    name,\n    description,\n    stargazers_count,\n    forks_count,\n    open_issues_count,\n    html_url,\n    language,\n    owner: { avatar_url, html_url: profile_url },\n  } = repo;\n\n  return (\n    <div className=\"w-1/3 pl-6 mb-6\" data-id=\"repo-card\">\n      <div\n        className={cx(\n          'h-64 flex flex-col px-5 py-4 shadow-lg rounded-lg',\n          settingsStore.isDark\n            ? 'bg-gray-800 text-white'\n            : 'bg-white text-gray-900'\n        )}\n      >\n        <div className=\"flex items-center min-h-10\">\n          <a\n            href={profile_url}\n            className=\"inline-block h-8 w-8 overflow-hidden rounded-full mr-3\"\n          >\n            <img src={avatar_url} />\n          </a>\n          <a\n            className={cx(\n              'min-w-0 truncate hover:underline text-2xl py-2',\n              settingsStore.isDark\n                ? 'text-blue-400'\n                : 'text-blue-500 hover:text-blue-600'\n            )}\n            href={html_url}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            data-id=\"repo-link\"\n          >\n            {name}\n          </a>\n        </div>\n        <div className=\"leading-normal mt-1 overflow-hidden\">{description}</div>\n        <div\n          className={cx(\n            'flex-1 min-h-6 flex items-end mt-2',\n            settingsStore.isDark ? 'text-gray-500' : 'text-gray-700'\n          )}\n        >\n          {language && <div className=\"mr-5\">{language}</div>}\n          <div className=\"mr-5\">\n            <i className=\"fa fa-star\" />{' '}\n            {humanFormat(stargazers_count, { decimals: 1, separator: '' })}\n          </div>\n          <div className=\"mr-5\">\n            <i className=\"fa fa-code-branch\" />{' '}\n            {humanFormat(forks_count, { decimals: 1, separator: '' })}\n          </div>\n          <div>\n            <i className=\"fa fa-exclamation-circle\" />{' '}\n            {humanFormat(open_issues_count, { decimals: 1, separator: '' })}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/containers/RepoCard/index.ts",
    "content": "export { RepoCard } from './RepoCard';\n"
  },
  {
    "path": "src/containers/SettingsModal/Panel.tsx",
    "content": "import { find } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport { SettingsStore } from '../../store/settings';\nimport { SETTINGS_VIEWS } from './constants';\n\ninterface IProps {\n  selectedTab: string;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const Panel = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ selectedTab, settingsStore }) => {\n  const view = find(SETTINGS_VIEWS, { id: selectedTab });\n\n  if (!view) {\n    return null;\n  }\n\n  const Component = view.component;\n\n  return <Component settings={view.isProvider ? undefined : settingsStore} />;\n});\n"
  },
  {
    "path": "src/containers/SettingsModal/SettingsModal.tsx",
    "content": "import React, { useState } from 'react';\nimport styled from 'styled-components';\n\nimport { Modal } from '../../components/Modal';\nimport { SETTINGS_VIEWS } from './constants';\nimport { Panel } from './Panel';\nimport { Sidebar } from './Sidebar';\n\nconst Container = styled.div`\n  width: 900px;\n`;\n\ninterface IProps {\n  onClose: () => void;\n}\n\nexport function SettingsModal({ onClose }: IProps) {\n  const [selectedTab, selectTab] = useState(SETTINGS_VIEWS[0].id);\n\n  return (\n    <Modal onClose={onClose}>\n      <Container className=\"mt-32 mx-auto flex\">\n        <Sidebar selectedTab={selectedTab} selectTab={selectTab} />\n        <Panel selectedTab={selectedTab} />\n      </Container>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "src/containers/SettingsModal/Sidebar.tsx",
    "content": "import cx from 'classnames';\nimport { partition } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\nimport styled from 'styled-components';\n\nimport { SettingsStore } from '../../store/settings';\nimport { SETTINGS_VIEWS } from './constants';\nimport { ISettingView } from './types';\n\nconst Wrapper = styled.div`\n  width: 200px;\n  font-family: Roboto;\n  padding-right: 30px;\n  flex-shrink: 0;\n`;\n\nconst ItemHeader = styled.div`\n  font-size: 16px;\n  font-weight: 500;\n  letter-spacing: 0.1em;\n  padding-left: 12px;\n  margin-bottom: 14px;\n`;\n\nconst Item = styled.div`\n  font-size: 18px;\n  padding: 6px;\n  padding-left: 12px;\n  margin-bottom: 10px;\n  cursor: pointer;\n`;\n\ninterface IProps {\n  selectedTab: string;\n  selectTab: Function;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const Sidebar = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ selectedTab, selectTab, settingsStore }) => {\n  const [providerItems, staticItems] = partition(SETTINGS_VIEWS, 'isProvider');\n\n  function renderItems(items: ISettingView[]) {\n    return items.map(({ label, id }) => (\n      <Item\n        key={id}\n        onClick={() => selectTab(id)}\n        className={cx(\n          selectedTab === id && 'text-white bg-blue-500 font-medium rounded',\n          settingsStore.isDark && 'text-gray-300'\n        )}\n        data-setting-tab={label}\n      >\n        {label}\n      </Item>\n    ));\n  }\n\n  const headerClass = settingsStore.isDark ? 'text-gray-600' : 'text-gray-500';\n\n  return (\n    <Wrapper>\n      <ItemHeader className={headerClass}>Settings</ItemHeader>\n      {renderItems(staticItems)}\n      <ItemHeader className={cx('mt-10', headerClass)}>Providers</ItemHeader>\n      {renderItems(providerItems)}\n    </Wrapper>\n  );\n});\n"
  },
  {
    "path": "src/containers/SettingsModal/constants.ts",
    "content": "import { map } from 'lodash';\n\nimport { providers } from '../../providers';\nimport { CacheSettings, NightMode } from './tabs';\nimport { ISettingView } from './types';\n\nexport const SETTINGS_VIEWS: ISettingView[] = [\n  {\n    id: 'night_mode',\n    label: 'Night mode',\n    component: NightMode,\n  },\n  {\n    id: 'cache',\n    label: 'Cache',\n    component: CacheSettings,\n  },\n  ...map(providers, ({ label, settingsComponent }, key) => ({\n    id: key,\n    label,\n    component: settingsComponent,\n    isProvider: true,\n  })),\n];\n"
  },
  {
    "path": "src/containers/SettingsModal/index.ts",
    "content": "export { SettingsModal } from './SettingsModal';\n"
  },
  {
    "path": "src/containers/SettingsModal/tabs/Cache.tsx",
    "content": "import React from 'react';\n\nimport { Button, ButtonType } from '../../../components/Button';\nimport { toast } from '../../../components/ToastManager';\nimport { Cache } from '../../../lib/cache';\n\nexport const CacheSettings = () => {\n  function flushCache() {\n    Cache.flush();\n    toast('Cache was successfully cleared', 'info');\n  }\n\n  return (\n    <div>\n      <div className=\"font-medium mb-4\">Cached data</div>\n      <p className=\"leading-normal\">\n        In order to limit the amount of requests to the various providers, a lot\n        of them are actually cached. If for some reason you noticed\n        inconsistencies or outdated data, please file an issue.\n      </p>\n      <p className=\"mt-3\">\n        In the meantime, you can clear the cached data to get back to a coherent\n        state.\n      </p>\n      <Button type={ButtonType.PRIMARY} onClick={flushCache} className=\"mt-4\">\n        Clear cache\n      </Button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/containers/SettingsModal/tabs/NightMode.tsx",
    "content": "import React, { useEffect, useState } from 'react';\n\nimport { RadioCard } from '../../../components/RadioCard';\nimport { DARK_MODE } from '../../../constants/darkMode';\nimport { SettingsStore } from '../../../store/settings';\n\ninterface IProps {\n  settings: SettingsStore;\n}\n\nexport const NightMode = ({ settings }: IProps) => {\n  const [darkMode, setDarkMode] = useState(settings.darkMode);\n\n  useEffect(() => settings.updateDarkMode(darkMode), [darkMode]);\n\n  return (\n    <div>\n      <div>\n        <div className=\"font-medium mb-4\">\n          When should dark mode be enabled?\n        </div>\n        <RadioCard\n          title=\"Never\"\n          text=\"The interface will stay bright, no matter what time of the day or night it is.\"\n          icon=\"fas fa-sun\"\n          selected={darkMode === DARK_MODE.DISABLED}\n          onClick={() => setDarkMode(DARK_MODE.DISABLED)}\n          dark={settings.isDark}\n        />\n        <RadioCard\n          title=\"Always\"\n          text=\"The interface will always be dark, protecting your eyes from bright lights. It’s really useful at night.\"\n          icon=\"fas fa-moon\"\n          selected={darkMode === DARK_MODE.ENABLED}\n          onClick={() => setDarkMode(DARK_MODE.ENABLED)}\n          dark={settings.isDark}\n        />\n        <RadioCard\n          title=\"Only at night (from 7PM to 7AM)\"\n          text=\"The perfect compromise between the two other options. Bright and legible during the day, but dark at night.\"\n          icon=\"fas fa-clock\"\n          selected={darkMode === DARK_MODE.AT_NIGHT}\n          onClick={() => setDarkMode(DARK_MODE.AT_NIGHT)}\n          dark={settings.isDark}\n        />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/containers/SettingsModal/tabs/index.ts",
    "content": "export { NightMode } from './NightMode';\nexport { CacheSettings } from './Cache';\n"
  },
  {
    "path": "src/containers/SettingsModal/types.ts",
    "content": "export interface ISettingView {\n  id: string;\n  label: string;\n  component: any;\n  isProvider?: boolean;\n}\n"
  },
  {
    "path": "src/containers/index.ts",
    "content": "export { RepoCard } from './RepoCard';\nexport { FilterEditModal } from './FilterEditModal';\nexport { SettingsModal } from './SettingsModal';\n"
  },
  {
    "path": "src/errors/InvalidCredentials.ts",
    "content": "import ExtendableError from 'es6-error';\n\nexport class InvalidCredentials extends ExtendableError {\n  constructor(message = 'Your token seems to be invalid') {\n    super(message);\n  }\n}\n"
  },
  {
    "path": "src/errors/NeedTokenError.ts",
    "content": "import ExtendableError from 'es6-error';\n\nexport class NeedTokenError extends ExtendableError {\n  constructor(message = 'Fetching failed. Try to set a Github token') {\n    super(message);\n  }\n}\n"
  },
  {
    "path": "src/errors/RateLimitError.ts",
    "content": "import ExtendableError from 'es6-error';\n\nexport class RateLimitError extends ExtendableError {\n  public remainingRateLimit: number;\n\n  constructor(remainingRateLimit: number) {\n    super('Rate limited. Set a Github token to raise the limit');\n    this.remainingRateLimit = Math.round(remainingRateLimit);\n  }\n}\n"
  },
  {
    "path": "src/errors/index.ts",
    "content": "export { NeedTokenError } from './NeedTokenError';\nexport { RateLimitError } from './RateLimitError';\nexport { InvalidCredentials } from './InvalidCredentials';\n"
  },
  {
    "path": "src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <title>OctoLenses - Github Dashboard</title>\n    <link rel=\"stylesheet\" href=\"./index.scss\" />\n    <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.6.0/css/all.css\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n</head>\n\n<body>\n    <div id=\"container\"></div>\n    <script type=\"module\" src=\"index.tsx\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/index.scss",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@import url('https://fonts.googleapis.com/css?family=Open+Sans|Roboto:400,500');\n\nhtml {\n  line-height: 1.15;\n}\n\nbody {\n  @apply text-gray-900 font-open;\n  @apply m-0 min-h-screen;\n  @apply bg-gray-100;\n  font-size: 100%;\n\n  &.dark {\n    @apply bg-gray-900 text-white;\n  }\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  @apply font-roboto;\n}\n\na,\na:hover,\na:active,\na:visited {\n  @apply no-underline;\n}\n\n#container {\n  @apply min-h-full mx-auto;\n  @apply px-8;\n  max-width: 1180px;\n\n  .App {\n    @apply min-h-full flex flex-col;\n  }\n}\n\n.rtl {\n  direction: rtl;\n}\n"
  },
  {
    "path": "src/index.tsx",
    "content": "import 'babel-polyfill';\nimport { Provider } from 'mobx-react';\nimport React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport { App } from './App';\nimport {\n  bootstrap,\n  filtersStore,\n  navigationStore,\n  settingsStore,\n  trendsStore,\n} from './store';\n\nbootstrap();\n\nReactDOM.render(\n  <Provider\n    navigationStore={navigationStore}\n    settingsStore={settingsStore}\n    trendsStore={trendsStore}\n    filtersStore={filtersStore}\n  >\n    <App />\n  </Provider>,\n  document.getElementById('container')\n);\n"
  },
  {
    "path": "src/lib/assertUnreachable.ts",
    "content": "export function assertUnreachable<T>(_: never, defaultValue: T): T {\n  return defaultValue;\n}\n"
  },
  {
    "path": "src/lib/cache.ts",
    "content": "import { chain, startsWith } from 'lodash';\n\ninterface ICacheEntry<T> {\n  expiresAt: number;\n  value: T;\n}\n\nexport class Cache {\n  public static prefix = 'cache';\n\n  /**\n   * Remember the result of an expensive computation or data fetching, during a\n   * certain period. The result is identified by a unique key.\n   * @param key Key at which to store the result of the computation\n   * @param lifespan How long, in second, to store the item\n   * @param computer The method that computes the value, if not in the cache\n   */\n  public static async remember<T = any>(\n    key: string,\n    lifespan: number,\n    computer: () => Promise<T>\n  ): Promise<T> {\n    const cached = Cache.get<T>(key);\n\n    if (cached) {\n      if (Date.now() < cached.expiresAt) {\n        return cached.value;\n      }\n\n      Cache.forget(key);\n    }\n\n    const value = await computer();\n    Cache.put(key, value, lifespan);\n    return value;\n  }\n\n  /**\n   * Retrieve an item from the cache\n   * @param key Key at which the item is stored\n   */\n  public static get<T = any>(key: string): ICacheEntry<T> {\n    const item = localStorage.getItem(Cache.getPrefixedKey(key));\n\n    try {\n      return JSON.parse(item);\n    } catch (error) {\n      // Do nothing\n    }\n\n    return null;\n  }\n\n  /**\n   * Put an item into the cache\n   * @param key Key at which to store the cached item\n   * @param value Item to cache\n   * @param lifespan How long, in seconds, we want to keep the item\n   */\n  public static put(key: string, value: any, lifespan: number) {\n    localStorage.setItem(\n      Cache.getPrefixedKey(key),\n      JSON.stringify({\n        expiresAt: Date.now() + lifespan * 1000,\n        value,\n      })\n    );\n  }\n\n  /**\n   * Forget a cached item\n   * @param key Key at which the item is stored\n   */\n  public static forget(key: string) {\n    localStorage.removeItem(Cache.getPrefixedKey(key));\n  }\n\n  /**\n   * Flush the whole cache\n   */\n  public static flush() {\n    Cache.getCacheKeys().forEach(Cache.forget);\n  }\n\n  /**\n   * Flush all the expired entries from the cache\n   */\n  public static flushExpired() {\n    Cache.getCacheKeys()\n      .filter(key => Cache.get(key).expiresAt < Date.now())\n      .forEach(Cache.forget);\n  }\n\n  /**\n   * Return a prefixed key\n   * @param key\n   */\n  private static getPrefixedKey(key: string) {\n    if (Cache.isPrefixed(key)) {\n      return key;\n    }\n\n    return `${Cache.prefix}:${key}`;\n  }\n\n  /**\n   * Return whether a key is already prefixed or not\n   * @param key\n   */\n  private static isPrefixed(key: string) {\n    return startsWith(key, `${Cache.prefix}:`);\n  }\n\n  /**\n   * Return all the keys stored in the cache\n   */\n  private static getCacheKeys() {\n    const cachePrefix = `${Cache.prefix}:`;\n    return chain(localStorage)\n      .keys()\n      .filter(key => startsWith(key, cachePrefix))\n      .value();\n  }\n}\n"
  },
  {
    "path": "src/lib/github/index.ts",
    "content": "export { fetchTrendingRepos } from './trending';\n"
  },
  {
    "path": "src/lib/github/trending/index.ts",
    "content": "import hash from 'object-hash';\n\nimport { Cache } from '../../../lib/cache';\nimport { client } from '../../../providers/github/fetchers/client';\n\ninterface IFetchTrendingReposParams {\n  language: string;\n  date: string;\n  token?: string;\n}\n\n/**\n * Fetch the trending repositories from GitHub\n * @param language What programming language the user is interested in\n * @param date From which date\n * @param token Github token of the user\n */\nexport const fetchTrendingRepos = async ({\n  language,\n  date,\n  token,\n}: IFetchTrendingReposParams) => {\n  let query = `created:>${date}`;\n  if (language !== null) {\n    query += ` and language:${language}`;\n  }\n\n  const cacheKey = `github.trending.${hash(query)}`;\n  const { items: repos } = await Cache.remember(cacheKey, 60 * 60, () =>\n    client({\n      endpoint: '/search/repositories',\n      qs: `per_page=100&q=${query}&sort=stars&order=desc`,\n      token,\n    })\n  );\n\n  return repos;\n};\n"
  },
  {
    "path": "src/migrations/index.ts",
    "content": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport { IMigration } from './types';\nimport v0_to_v1 from './v0-to-v1';\nimport v1_to_v2 from './v1-to-v2';\nimport v2_to_v3 from './v2-to-v3';\n\nimport './testing-utils';\n\nclass Migrator {\n  private migrations: IMigration[] = [];\n\n  public migrate() {\n    console.log('[migration] Running necessary migrations...');\n\n    this.migrations.forEach(migration => {\n      if (migration.shouldRun()) {\n        console.log(`[migration] Running migration ${migration.name}`);\n        migration.run();\n      }\n    });\n  }\n\n  public registerMigration(migration: IMigration): Migrator {\n    this.migrations.push(migration);\n    return this;\n  }\n}\n\nexport const migrator = new Migrator()\n  .registerMigration(new v0_to_v1())\n  .registerMigration(new v1_to_v2())\n  .registerMigration(new v2_to_v3());\n"
  },
  {
    "path": "src/migrations/mocks/index.ts",
    "content": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport v0 from './v0';\nimport v1 from './v1';\nimport v1_withoutToken from './v1-without-token';\nimport v2 from './v2';\nimport v2_withNegatedPredicates from './v2-with-negated-predicates';\nimport v3_withDefaultedOperators from './v3-with-defaulted-operators';\n\nconst allMocks: Record<string, object> = {\n  v0,\n  v1,\n  v1_withoutToken,\n  v2,\n  v2_withNegatedPredicates,\n  v3_withDefaultedOperators,\n};\n\nexport default allMocks;\n"
  },
  {
    "path": "src/migrations/mocks/v0.ts",
    "content": "export default {\n  filtersStore: {\n    data: [\n      {\n        id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n        label: 'OctoLenses Issues',\n        predicates: [\n          { type: 'type', value: 'issues' },\n          { type: 'repo', value: 'rgehan/octolenses', negated: false },\n          { type: 'status', value: 'open' },\n        ],\n      },\n      {\n        id: '92da29c0-5216-11e9-ad7a-73a635a52ed2',\n        label: 'My Private Filter',\n        predicates: [\n          { type: 'status', value: 'open' },\n          {\n            type: 'repo',\n            value: 'botify-hq/botify-report',\n            negated: false,\n          },\n          { type: 'author', value: 'rgehan', negated: false },\n          { type: 'status', value: 'open', negated: false },\n        ],\n      },\n    ],\n  },\n  settingsStore: {\n    language: null,\n    dateRange: 'last_week',\n    token: '<token>',\n    wasOnboarded: true,\n    darkMode: 'DISABLED',\n  },\n  useNewTabPage: true,\n};\n"
  },
  {
    "path": "src/migrations/mocks/v1-without-token.ts",
    "content": "export default {\n  filtersStore: {\n    data: [\n      {\n        id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n        label: 'OctoLenses Issues',\n        predicates: [\n          { type: 'type', value: 'issues' },\n          { type: 'repo', value: 'rgehan/octolenses', negated: false },\n          { type: 'status', value: 'open' },\n        ],\n      },\n      {\n        id: '92da29c0-5216-11e9-ad7a-73a635a52ed2',\n        label: 'My Private Filter',\n        predicates: [\n          { type: 'status', value: 'open' },\n          {\n            type: 'repo',\n            value: 'botify-hq/botify-report',\n            negated: false,\n          },\n          { type: 'author', value: 'rgehan', negated: false },\n          { type: 'status', value: 'open', negated: false },\n        ],\n      },\n    ],\n  },\n  settingsStore: {\n    language: null,\n    dateRange: 'last_week',\n    wasOnboarded: true,\n    darkMode: 'DISABLED',\n    schemaVersion: 1,\n  },\n  useNewTabPage: true,\n};\n"
  },
  {
    "path": "src/migrations/mocks/v1.ts",
    "content": "export default {\n  filtersStore: {\n    data: [\n      {\n        id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n        label: 'OctoLenses Issues',\n        predicates: [\n          { type: 'type', value: 'issues' },\n          { type: 'repo', value: 'rgehan/octolenses', negated: false },\n          { type: 'status', value: 'open' },\n        ],\n      },\n      {\n        id: '92da29c0-5216-11e9-ad7a-73a635a52ed2',\n        label: 'My Private Filter',\n        predicates: [\n          { type: 'status', value: 'open' },\n          {\n            type: 'repo',\n            value: 'botify-hq/botify-report',\n            negated: false,\n          },\n          { type: 'author', value: 'rgehan', negated: false },\n          { type: 'status', value: 'open', negated: false },\n        ],\n      },\n    ],\n  },\n  settingsStore: {\n    language: null,\n    dateRange: 'last_week',\n    token: '<token>',\n    wasOnboarded: true,\n    darkMode: 'DISABLED',\n    schemaVersion: 1,\n  },\n  useNewTabPage: true,\n};\n"
  },
  {
    "path": "src/migrations/mocks/v2-with-negated-predicates.ts",
    "content": "export default {\n  filtersStore: {\n    data: [\n      {\n        id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n        label: 'OctoLenses Issues',\n        provider: 'github',\n        predicates: [\n          { type: 'type', value: 'issues' },\n          { type: 'repo', value: 'rgehan/octolenses', negated: true },\n          { type: 'status', value: 'open', negated: false },\n          { type: 'author', value: 'rgehan', negated: false },\n        ],\n      },\n    ],\n  },\n  settingsStore: {\n    language: null,\n    dateRange: 'last_week',\n    wasOnboarded: true,\n    darkMode: 'DISABLED',\n    schemaVersion: 2,\n  },\n  useNewTabPage: true,\n  githubProvider: {\n    settings: {\n      token: '<token>',\n    },\n  },\n};\n"
  },
  {
    "path": "src/migrations/mocks/v2.ts",
    "content": "export default {\n  filtersStore: {\n    data: [\n      {\n        id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n        label: 'OctoLenses Issues',\n        provider: 'github',\n        predicates: [\n          { type: 'type', value: 'issues' },\n          { type: 'repo', value: 'rgehan/octolenses', negated: false },\n          { type: 'status', value: 'open' },\n        ],\n      },\n      {\n        id: '92da29c0-5216-11e9-ad7a-73a635a52ed2',\n        label: 'My Private Filter',\n        provider: 'github',\n        predicates: [\n          { type: 'status', value: 'open' },\n          {\n            type: 'repo',\n            value: 'botify-hq/botify-report',\n            negated: false,\n          },\n          { type: 'author', value: 'rgehan', negated: false },\n          { type: 'status', value: 'open', negated: false },\n        ],\n      },\n    ],\n  },\n  settingsStore: {\n    language: null,\n    dateRange: 'last_week',\n    wasOnboarded: true,\n    darkMode: 'DISABLED',\n    schemaVersion: 2,\n  },\n  useNewTabPage: true,\n  githubProvider: {\n    settings: {\n      token: '<token>',\n    },\n  },\n};\n"
  },
  {
    "path": "src/migrations/mocks/v3-with-defaulted-operators.ts",
    "content": "export default {\n  filtersStore: {\n    data: [\n      {\n        id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n        label: 'OctoLenses Issues',\n        provider: 'github',\n        predicates: [\n          { type: 'type', value: 'issues' },\n          { type: 'repo', value: 'rgehan/octolenses', operator: 'not_equal' },\n          { type: 'status', value: 'open' },\n          { type: 'author', value: 'rgehan', operator: 'equal' },\n        ],\n      },\n    ],\n  },\n  settingsStore: {\n    language: null,\n    dateRange: 'last_week',\n    wasOnboarded: true,\n    darkMode: 'DISABLED',\n    schemaVersion: 3,\n  },\n  useNewTabPage: true,\n  githubProvider: {\n    settings: {\n      token: '<token>',\n    },\n  },\n};\n"
  },
  {
    "path": "src/migrations/testing-utils.ts",
    "content": "/* tslint:disable no-console */\n\nimport { forEach, keys, pick, reduce } from 'lodash';\n\nimport { migrator } from './index';\nimport mocks from './mocks';\nimport { hydrateLocalStorageFromObject } from './utils';\n\ndeclare global {\n  // eslint-disable-next-line @typescript-eslint/interface-name-prefix\n  interface Window {\n    loadTestLocalStorage(name: string): void;\n    migrate(): void;\n    exportConfiguration(): void;\n    loadConfiguration(json: string): void;\n  }\n}\n\nwindow.loadTestLocalStorage = (name: string) => {\n  if (!mocks[name]) {\n    console.log('No such test data. Available keys: ' + keys(mocks).join(', '));\n    return;\n  }\n\n  console.log(`Loading test data: ${name}`);\n  hydrateLocalStorageFromObject(mocks[name]);\n};\n\nwindow.migrate = () => {\n  migrator.migrate();\n};\n\n/**\n * Serializes the relevant data stores in an exportable format\n */\nwindow.exportConfiguration = () => {\n  const data = pick(localStorage, [\n    'settingsStore',\n    'filtersStore',\n    'githubProvider',\n    'jiraProvider',\n    'useNewTabPage',\n  ]);\n\n  const stringified = JSON.stringify(\n    reduce(\n      data,\n      (acc: any, value, key) => {\n        acc[key] = btoa(value);\n        return acc;\n      },\n      {}\n    )\n  );\n\n  console.log(\n    `In order to import that configuration, run:\\n` +\n      `\\twindow.loadConfiguration('${stringified}');`\n  );\n};\n\n/**\n * Imports a previously serialized data store\n */\nwindow.loadConfiguration = (json: string) => {\n  forEach(JSON.parse(json), (v, k) => {\n    localStorage.setItem(k, atob(v));\n  });\n\n  console.log('Import done.');\n};\n"
  },
  {
    "path": "src/migrations/types.ts",
    "content": "export interface IMigration {\n  name: string;\n  shouldRun(): boolean;\n  run(): void;\n}\n"
  },
  {
    "path": "src/migrations/utils.ts",
    "content": "export function getFromLocalStorage(key: string) {\n  const data = localStorage.getItem(key);\n\n  if (data) {\n    return JSON.parse(data);\n  }\n\n  return null;\n}\n\nexport function saveToLocalStorage(key: string, value: any) {\n  const data = JSON.stringify(value);\n  localStorage.setItem(key, data);\n}\n\nexport function hydrateLocalStorageFromObject(object: any) {\n  localStorage.clear();\n\n  Object.keys(object).forEach(key => {\n    saveToLocalStorage(key, object[key]);\n  });\n}\n\nexport function dumpLocalStorageToObject() {\n  const object: Record<string, any> = {};\n\n  Object.keys(localStorage).forEach(key => {\n    object[key] = getFromLocalStorage(key);\n  });\n\n  return object;\n}\n"
  },
  {
    "path": "src/migrations/v0-to-v1.test.ts",
    "content": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport {\n  dumpLocalStorageToObject,\n  hydrateLocalStorageFromObject,\n} from './utils';\nimport v0_to_v1 from './v0-to-v1';\n\nimport v0 from './mocks/v0';\nimport v1 from './mocks/v1';\n\ndescribe('v0 to v1 migrations', () => {\n  beforeEach(() => {\n    localStorage.clear();\n  });\n\n  it('should run if no schemaVersion is set', () => {\n    hydrateLocalStorageFromObject(v0);\n    expect(new v0_to_v1().shouldRun()).toEqual(true);\n  });\n\n  it('should not run if a schemaVersion is set', () => {\n    hydrateLocalStorageFromObject(v1);\n    expect(new v0_to_v1().shouldRun()).toEqual(false);\n  });\n\n  it('should add schemaVersion to the settings', () => {\n    hydrateLocalStorageFromObject(v0);\n    new v0_to_v1().run();\n    const obj = dumpLocalStorageToObject();\n    expect(obj).toHaveProperty('settingsStore.schemaVersion', 1);\n  });\n});\n"
  },
  {
    "path": "src/migrations/v0-to-v1.ts",
    "content": "/* tslint:disable no-console */\n\nimport { IMigration } from './types';\nimport { getFromLocalStorage, saveToLocalStorage } from './utils';\n\nexport default class implements IMigration {\n  public name = 'v0-to-v1';\n\n  public shouldRun(): boolean {\n    const settings = getFromLocalStorage('settingsStore');\n\n    if (settings && settings.schemaVersion === undefined) {\n      return true;\n    }\n\n    return false;\n  }\n\n  public run() {\n    const settings = getFromLocalStorage('settingsStore');\n    settings.schemaVersion = 1;\n    console.log('[migration] Upgrading schema version to 1');\n    saveToLocalStorage('settingsStore', settings);\n  }\n}\n"
  },
  {
    "path": "src/migrations/v1-to-v2.test.ts",
    "content": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport {\n  dumpLocalStorageToObject,\n  hydrateLocalStorageFromObject,\n} from './utils';\nimport v1_to_v2 from './v1-to-v2';\n\nimport v1 from './mocks/v1';\nimport v1_withoutToken from './mocks/v1-without-token';\nimport v2 from './mocks/v2';\n\ndescribe('v1 to v2 migrations', () => {\n  beforeEach(() => {\n    localStorage.clear();\n  });\n\n  it('should run if schemaVersion is 1', () => {\n    hydrateLocalStorageFromObject(v1);\n    expect(new v1_to_v2().shouldRun()).toEqual(true);\n  });\n\n  it('should not run if schemaVersion is not 1', () => {\n    hydrateLocalStorageFromObject(v2);\n    expect(new v1_to_v2().shouldRun()).toEqual(false);\n  });\n\n  it('should set all filters provider to \"github\" by default', () => {\n    hydrateLocalStorageFromObject(v1);\n\n    new v1_to_v2().run();\n\n    const obj = dumpLocalStorageToObject();\n    obj.filtersStore.data.map((filter: any) => {\n      expect(filter.provider).toEqual('github');\n    });\n  });\n\n  it('should not add a token if there is none yet', () => {\n    hydrateLocalStorageFromObject(v1_withoutToken);\n    expect(v1_withoutToken).not.toHaveProperty('settingsStore.token');\n\n    new v1_to_v2().run();\n\n    const obj = dumpLocalStorageToObject();\n    expect(obj).not.toHaveProperty('githubProvider.settings.token');\n    expect(obj).not.toHaveProperty('githubProvider');\n  });\n\n  it('should create a githubProvider.settings object containing the token', () => {\n    hydrateLocalStorageFromObject(v1);\n    expect(v1).toHaveProperty('settingsStore.token');\n\n    new v1_to_v2().run();\n\n    const obj = dumpLocalStorageToObject();\n    expect(obj).toHaveProperty('githubProvider.settings.token');\n  });\n\n  it('should set schemaVersion to 2 upon completion', () => {\n    hydrateLocalStorageFromObject(v1);\n\n    new v1_to_v2().run();\n\n    const obj = dumpLocalStorageToObject();\n    expect(obj).toHaveProperty('settingsStore.schemaVersion', 2);\n  });\n});\n"
  },
  {
    "path": "src/migrations/v1-to-v2.ts",
    "content": "/* tslint:disable no-console */\n\nimport { ProviderType } from '../providers';\nimport { Filter } from '../store/filters';\nimport { IMigration } from './types';\nimport { getFromLocalStorage, saveToLocalStorage } from './utils';\n\nexport default class implements IMigration {\n  public name = 'v1-to-v2';\n\n  public shouldRun(): boolean {\n    const settings = getFromLocalStorage('settingsStore');\n\n    if (settings && settings.schemaVersion === 1) {\n      return true;\n    }\n\n    return false;\n  }\n\n  public run() {\n    this.defaultFiltersProviderToGithub();\n    this.moveTokenFromSettingsToGithubProvider();\n    this.upgradeSchemaVersion();\n  }\n\n  private defaultFiltersProviderToGithub() {\n    const filters = getFromLocalStorage('filtersStore');\n\n    console.log('[migration] Defaulting all filters providers to \"github\"');\n    filters.data.forEach((filter: Filter) => {\n      filter.provider = filter.provider || ProviderType.GITHUB;\n    });\n\n    saveToLocalStorage('filtersStore', filters);\n  }\n\n  private moveTokenFromSettingsToGithubProvider() {\n    const settings = getFromLocalStorage('settingsStore');\n    const githubProvider = getFromLocalStorage('githubProvider');\n\n    if (githubProvider || !settings.token) {\n      return;\n    }\n\n    console.log('[migration] Moving set token to github provider settings');\n    saveToLocalStorage('githubProvider', {\n      settings: { token: settings.token },\n    });\n  }\n\n  private upgradeSchemaVersion() {\n    const settings = getFromLocalStorage('settingsStore');\n    settings.schemaVersion = 2;\n    console.log('[migration] Upgrading schema version to 2');\n    saveToLocalStorage('settingsStore', settings);\n  }\n}\n"
  },
  {
    "path": "src/migrations/v2-to-v3.test.ts",
    "content": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport {\n  dumpLocalStorageToObject,\n  hydrateLocalStorageFromObject,\n} from './utils';\nimport v2_to_v3 from './v2-to-v3';\n\nimport v2 from './mocks/v2-with-negated-predicates';\nimport v3 from './mocks/v3-with-defaulted-operators';\n\ndescribe('v2 to v3 migrations', () => {\n  beforeEach(() => {\n    localStorage.clear();\n  });\n\n  it('should run if schemaVersion is 2', () => {\n    hydrateLocalStorageFromObject(v2);\n    expect(new v2_to_v3().shouldRun()).toEqual(true);\n  });\n\n  it('should properly default operators', () => {\n    hydrateLocalStorageFromObject(v2);\n\n    new v2_to_v3().run();\n\n    const obj = dumpLocalStorageToObject();\n    expect(obj).toEqual(v3);\n  });\n\n  it('should set schemaVersion to 3 upon completion', () => {\n    hydrateLocalStorageFromObject(v2);\n\n    new v2_to_v3().run();\n\n    const obj = dumpLocalStorageToObject();\n    expect(obj).toHaveProperty('settingsStore.schemaVersion', 3);\n  });\n});\n"
  },
  {
    "path": "src/migrations/v2-to-v3.ts",
    "content": "/* tslint:disable no-console */\n\nimport { providers, ProviderType } from '../providers';\nimport { Filter } from '../store/filters';\nimport { IMigration } from './types';\nimport { getFromLocalStorage, saveToLocalStorage } from './utils';\n\nexport default class implements IMigration {\n  public name = 'v2-to-v3';\n\n  public shouldRun(): boolean {\n    const settings = getFromLocalStorage('settingsStore');\n\n    if (settings && settings.schemaVersion === 2) {\n      return true;\n    }\n\n    return false;\n  }\n\n  public run() {\n    this.defaultPredicatesOperators();\n    this.upgradeSchemaVersion();\n  }\n\n  private defaultPredicatesOperators() {\n    const filters = getFromLocalStorage('filtersStore');\n\n    // There shouldn't be any other provider in use for now\n    const provider = providers[ProviderType.GITHUB];\n\n    console.log('[migration] Defaulting all predicates operators');\n    filters.data.forEach((filter: Filter) => {\n      filter.predicates.forEach((predicate: any) => {\n        const isNegated = predicate.negated;\n\n        predicate.negated = undefined;\n\n        const predicateDefinition = provider.findPredicate(predicate.type);\n\n        // Shouldn't happen, but let's be safe\n        if (!predicateDefinition) {\n          return;\n        }\n\n        // Operator-less predicates don't have an `operator` key\n        if (predicateDefinition.operators.length === 0) {\n          return;\n        }\n\n        predicate.operator = isNegated ? 'not_equal' : 'equal';\n      });\n    });\n\n    saveToLocalStorage('filtersStore', filters);\n  }\n\n  private upgradeSchemaVersion() {\n    const settings = getFromLocalStorage('settingsStore');\n    settings.schemaVersion = 3;\n    console.log('[migration] Upgrading schema version to 3');\n    saveToLocalStorage('settingsStore', settings);\n  }\n}\n"
  },
  {
    "path": "src/pages/Dashboard/Dashboard.tsx",
    "content": "import cx from 'classnames';\nimport ExtendableError from 'es6-error';\nimport { get, size } from 'lodash';\nimport { computed } from 'mobx';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\n\nimport { Loader } from '../../components';\nimport { FilterEditModal } from '../../containers';\nimport { providers } from '../../providers';\nimport { FiltersStore } from '../../store/filters';\nimport { SettingsStore } from '../../store/settings';\nimport { FilterLinkContainer } from './FilterLinkContainer';\n\ninterface IProps {\n  filtersStore?: FiltersStore;\n  settingsStore?: SettingsStore;\n}\n\n@inject('filtersStore', 'settingsStore')\n@observer\nexport class Dashboard extends React.Component<IProps> {\n  public state = {\n    filterModal: { isOpen: false, mode: 'adding' },\n    metaPressed: false,\n  };\n\n  componentDidMount() {\n    window.addEventListener('keydown', this.handleKeyDown);\n    window.addEventListener('keyup', this.handleKeyUp);\n  }\n\n  componentWillUnmount() {\n    window.removeEventListener('keydown', this.handleKeyDown);\n    window.removeEventListener('keyup', this.handleKeyUp);\n  }\n\n  private handleKeyDown = (event: KeyboardEvent) => {\n    if (event.key === 'Meta') {\n      this.setState({ metaPressed: true });\n    }\n  };\n\n  private handleKeyUp = (event: KeyboardEvent) => {\n    if (event.key === 'Meta') {\n      this.setState({ metaPressed: false });\n    }\n  };\n\n  @computed\n  get selectedFilter() {\n    const { filtersStore, settingsStore } = this.props;\n\n    const filter = filtersStore.findFilter(settingsStore.selectedFilterId);\n    const firstFilter = filtersStore.getFirstFilter();\n    return filter || firstFilter;\n  }\n\n  public handleFilterSelected = (filterId: string) => {\n    const { filtersStore, settingsStore } = this.props;\n\n    if (filterId === settingsStore.selectedFilterId) {\n      return;\n    }\n\n    // Clear the notifications of the filter that was selected\n    filtersStore\n      .findFilter(settingsStore.selectedFilterId)\n      .clearNewItemsNotifications();\n\n    // Select the new filter\n    settingsStore.selectedFilterId = filterId;\n  };\n\n  public handleCloneFilter = () => {\n    const { filtersStore } = this.props;\n\n    const { id } = filtersStore.cloneFilter(this.selectedFilter.id);\n    this.handleFilterSelected(id);\n  };\n\n  public handleRefreshFilter = () => {\n    this.selectedFilter.invalidateCache();\n  };\n\n  public handleRefreshAllFilters = () => {\n    this.props.filtersStore.fetchAllFilters();\n  };\n\n  public handleDeleteFilter = () => {\n    const { filtersStore, settingsStore } = this.props;\n\n    if (!this.selectedFilter || filtersStore.count === 1) {\n      return;\n    }\n\n    // Find out the index (in the list) of the filter\n    const currentFilterIndex = filtersStore.findFilterIndex(\n      this.selectedFilter.id\n    );\n\n    // Find out which filter we'll have to select once removed\n    const isDeletingLastFilter = currentFilterIndex === filtersStore.count - 1;\n    const newlySelectedFilterIndex = isDeletingLastFilter\n      ? currentFilterIndex - 1\n      : currentFilterIndex + 1;\n\n    // Find the actual UUID of the filter\n    const realIndex = filtersStore.getFilterAt(newlySelectedFilterIndex).id;\n\n    // Remove the filter, then select the next one\n    filtersStore.removeFilter(this.selectedFilter.id);\n    settingsStore.selectedFilterId = realIndex;\n  };\n\n  /*\n   * Modal logic\n   */\n\n  public handleOpenFilterModal = (mode: string) => () => {\n    this.setState({\n      filterModal: {\n        isOpen: true,\n        mode,\n      },\n    });\n  };\n\n  public handleCloseFilterModal = () => {\n    this.setState({\n      filterModal: {\n        isOpen: false,\n      },\n    });\n  };\n\n  public reorderFilters = ({ oldIndex, newIndex }: any) => {\n    const { filtersStore } = this.props;\n\n    // Do nothing if the user cancelled the drag\n    if (oldIndex === newIndex) {\n      return;\n    }\n\n    // Select the filter we want to move...\n    this.handleFilterSelected(filtersStore.getFilterAt(oldIndex).id);\n\n    // ...and move it\n    filtersStore.swapFilters(oldIndex, newIndex);\n  };\n\n  public render() {\n    const { filtersStore, settingsStore } = this.props;\n    const { filterModal, metaPressed } = this.state;\n\n    const LINKS = [\n      {\n        handler: this.handleOpenFilterModal('adding'),\n        text: 'Add',\n        icon: 'far fa-plus-square',\n      },\n      {\n        handler: this.handleOpenFilterModal('editing'),\n        text: 'Edit',\n        icon: 'far fa-edit',\n      },\n      {\n        handler: this.handleCloneFilter,\n        text: 'Clone',\n        icon: 'far fa-clone',\n      },\n      metaPressed\n        ? {\n            handler: this.handleRefreshAllFilters,\n            text: 'Refresh All',\n            icon: 'fas fa-sync-alt',\n          }\n        : {\n            handler: this.handleRefreshFilter,\n            text: 'Refresh',\n            icon: 'fas fa-sync-alt',\n          },\n      {\n        handler: this.handleDeleteFilter,\n        text: 'Delete',\n        icon: 'far fa-trash-alt',\n      },\n    ];\n\n    return (\n      <div className=\"flex items-start w-full h-full pt-16\">\n        <div className=\"flex flex-col w-48 sticky top-4\">\n          <FilterLinkContainer\n            links={filtersStore.getFilters()}\n            selectedFilterId={get(this.selectedFilter, 'id')}\n            onFilterSelected={this.handleFilterSelected}\n            onSortEnd={this.reorderFilters}\n            lockAxis=\"y\"\n            lockToContainerEdges\n            useDragHandle\n          />\n          <div className=\"flex flex-col items-end pr-5 mt-10\">\n            {LINKS.map(({ handler, text, icon }) => (\n              <div\n                onClick={handler}\n                key={text}\n                className={cx(\n                  'mb-3 cursor-pointer select-none text-gray-600',\n                  settingsStore.isDark\n                    ? 'hover:text-gray-500'\n                    : 'hover:text-gray-900'\n                )}\n              >\n                {text} <i className={cx(icon, 'ml-1 w-6 opacity-75')} />\n              </div>\n            ))}\n          </div>\n        </div>\n        <div\n          className={cx(\n            'flex-1 flex flex-col shadow-xl rounded-lg mb-16 min-w-0',\n            settingsStore.isDark ? 'bg-gray-800 text-white' : 'bg-white'\n          )}\n          data-id=\"filter-results\"\n        >\n          {this.renderResults()}\n        </div>\n        {filterModal.isOpen && (\n          <FilterEditModal\n            initialFilter={\n              filterModal.mode === 'editing' ? this.selectedFilter : null\n            }\n            onClose={this.handleCloseFilterModal}\n          />\n        )}\n      </div>\n    );\n  }\n\n  public renderResults() {\n    if (!this.selectedFilter) {\n      return null;\n    }\n\n    if (this.selectedFilter.loading) {\n      return <Loader size={50} className=\"my-10\" />;\n    }\n\n    if (this.selectedFilter.error) {\n      const errorMessage =\n        this.selectedFilter.error instanceof ExtendableError\n          ? this.selectedFilter.error.message\n          : 'Something failed, sorry.';\n\n      return (\n        <div className=\"flex-1 flex items-center justify-center select-none text-2xl my-10 mx-0 text-gray-500\">\n          <i className=\"fas fa-exclamation-triangle mr-2\" />\n          {errorMessage}\n        </div>\n      );\n    }\n\n    if (size(this.selectedFilter.data) === 0) {\n      return (\n        <div className=\"flex-1 flex items-center justify-center select-none text-2xl my-10 mx-0 text-gray-500\">\n          <i className=\"fa fa-search mr-2\" />\n          No results.\n        </div>\n      );\n    }\n\n    const CardComponent = providers[this.selectedFilter.provider].cardComponent;\n\n    return this.selectedFilter.data.map((itemData, index) => (\n      <CardComponent\n        key={index}\n        data={itemData}\n        isNew={this.selectedFilter.isItemNew(itemData)}\n      />\n    ));\n  }\n}\n"
  },
  {
    "path": "src/pages/Dashboard/FilterLinkContainer.tsx",
    "content": "import React from 'react';\nimport { SortableContainer } from 'react-sortable-hoc';\n\nimport { FilterLink } from '../../components';\nimport { Filter } from '../../store/filters';\n\ninterface IProps {\n  links: Filter[];\n  selectedFilterId: string;\n  onFilterSelected: (id: string) => void;\n}\n\nexport const FilterLinkContainer = SortableContainer<IProps>(\n  ({ links, selectedFilterId, onFilterSelected }: IProps) => (\n    <div data-id=\"filter-links\">\n      {links.map((link: Filter, index: number) => (\n        <FilterLink\n          key={link.id}\n          index={index}\n          filter={link}\n          isSelected={link.id === selectedFilterId}\n          onClick={() => onFilterSelected(link.id)}\n        />\n      ))}\n    </div>\n  )\n);\n"
  },
  {
    "path": "src/pages/Dashboard/index.ts",
    "content": "export { Dashboard } from './Dashboard';\n"
  },
  {
    "path": "src/pages/Discover/Discover.scss",
    "content": ".Discover {\n  width: 100%;\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n\n  &__Actions {\n    display: flex;\n    flex-direction: row;\n    justify-content: flex-end;\n    margin-bottom: 20px;\n\n    > div {\n      margin-left: 20px;\n    }\n  }\n\n  &__ReposList {\n    flex: 1;\n    display: flex;\n\n    @media screen and (max-width: 768px) {\n      flex-direction: column;\n    }\n\n    @media screen and (min-width: 769px) {\n      flex-wrap: wrap;\n    }\n  }\n}\n"
  },
  {
    "path": "src/pages/Discover/Discover.tsx",
    "content": "import { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport { Dropdown, Loader } from '../../components';\nimport { DATES, DateType } from '../../constants/dates';\nimport { LANGUAGES } from '../../constants/languages';\nimport { RepoCard } from '../../containers';\nimport { SettingsStore } from '../../store/settings';\nimport { TrendsStore } from '../../store/trends';\n\nimport './Discover.scss';\n\ninterface IInnerProps {\n  settingsStore?: SettingsStore;\n  trendsStore?: TrendsStore;\n}\n\nexport const Discover = compose<IInnerProps, {}>(\n  inject('settingsStore', 'trendsStore'),\n  observer\n)(({ settingsStore, trendsStore }) => {\n  function handleChangeLanguage({ value }: { value: string }) {\n    settingsStore.updateLanguage(value);\n    trendsStore.fetchTrendingRepos();\n  }\n\n  function handleChangeDateRange({ value }: { value: string }) {\n    settingsStore.updateDateRange(value as DateType);\n    trendsStore.fetchTrendingRepos();\n  }\n\n  return (\n    <div className=\"Discover h-full w-full flex flex-col\">\n      <div className=\"flex items-end justify-end pb-4 h-16\">\n        <Dropdown\n          name=\"language\"\n          items={LANGUAGES}\n          value={settingsStore.language}\n          onChange={handleChangeLanguage}\n          className=\"mr-4\"\n        />\n        <Dropdown\n          name=\"dateRange\"\n          items={DATES}\n          value={settingsStore.dateRange}\n          onChange={handleChangeDateRange}\n        />\n      </div>\n      <div className=\"Discover__ReposList -ml-6\">\n        {trendsStore.loading ? (\n          <Loader />\n        ) : (\n          trendsStore.data.map(repo => <RepoCard key={repo.id} repo={repo} />)\n        )}\n      </div>\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/pages/Discover/index.ts",
    "content": "export { Discover } from './Discover';\n"
  },
  {
    "path": "src/pages/index.ts",
    "content": "export { Discover } from './Discover';\nexport { Dashboard } from './Dashboard';\n"
  },
  {
    "path": "src/providers/AbstractProvider.ts",
    "content": "import { observable } from 'mobx';\nimport { persist } from 'mobx-persist';\n\nimport { Filter } from '../store/filters';\nimport { SettingsStore } from '../store/settings';\nimport { Predicate } from './types';\n\ninterface ISettingsComponentProps {\n  settings: SettingsStore;\n}\n\ntype SettingsComponent = ({ settings }: ISettingsComponentProps) => JSX.Element;\ntype CardComponent = React.ComponentType<{ data: any }>;\n\nexport abstract class AbstractProvider<T = {}> {\n  /**\n   * Unique identifier of the provider\n   */\n  public id: string;\n\n  /**\n   * Name of the provider, as displayed in the Settings sidebar\n   */\n  public label: string;\n\n  /**\n   * Component used to render the provider settings panel\n   */\n  public settingsComponent: SettingsComponent;\n\n  /**\n   * Component used to render the provider's data on the dashboard\n   */\n  public cardComponent: CardComponent;\n\n  /**\n   * A place for the provider to store its specific settings\n   */\n  @persist('object')\n  @observable\n  public settings: T = {} as T;\n\n  /**\n   * Called after the app has been booted, so that we can perform initial\n   * data fetching and initialization tasks.\n   */\n  public abstract initialize(): Promise<void>;\n\n  /**\n   * Returns an array of available predicates for the provider\n   */\n  public abstract getAvailablePredicates(): Predicate[];\n\n  /**\n   * Retrieve the definition of a predicate from its name\n   * @param name Name of the predicate we want to retrieve\n   */\n  public abstract findPredicate(name: string): Predicate;\n\n  /**\n   * Fetch a filter associated to this provider\n   * @param filter\n   * @param providerSettings\n   */\n  public abstract fetchFilter(filter: Filter): Promise<any[]>;\n\n  /**\n   * Resolve the unique identifier associated to a filter item\n   * @param item A filter item\n   */\n  public abstract resolveFilterItemIdentifier(item: any): string;\n}\n"
  },
  {
    "path": "src/providers/github/components/IssueCard/CheckStatusIndicator.tsx",
    "content": "import cx from 'classnames';\nimport React from 'react';\n\nimport { IIssue } from './IssueCard';\nimport { IssueStatus } from './types';\n\ninterface IProps {\n  status: IIssue['status'];\n}\n\nconst STATUS_TO_ICON = {\n  [IssueStatus.SUCCESS]: 'fas fa-check text-green-500',\n  [IssueStatus.FAILURE]: 'fas fa-times text-red-500',\n  [IssueStatus.PENDING]: 'fas fa-circle text-orange-500',\n};\n\nconst STATUS_TO_LABEL = {\n  [IssueStatus.SUCCESS]: 'All checks passed',\n  [IssueStatus.FAILURE]: 'Some checks have failed',\n  [IssueStatus.PENDING]: 'Checks are running',\n};\n\nexport const CheckStatusIndicator = ({ status }: IProps) => {\n  if (status === IssueStatus.UNKNOWN) {\n    return null;\n  }\n\n  const icon = STATUS_TO_ICON[status];\n  const label = STATUS_TO_LABEL[status];\n\n  return <i className={cx('text-sm ml-2', icon)} title={label} />;\n};\n"
  },
  {
    "path": "src/providers/github/components/IssueCard/ConflictIndicator.tsx",
    "content": "import React from 'react';\n\ninterface IProps {\n  conflicting: boolean;\n}\n\nexport const ConflictIndicator = ({ conflicting }: IProps) => {\n  if (!conflicting) {\n    return null;\n  }\n\n  return (\n    <i\n      className=\"fas fa-exclamation-triangle text-red-500 text-sm ml-2\"\n      title=\"There are conflicts on this PR\"\n    />\n  );\n};\n"
  },
  {
    "path": "src/providers/github/components/IssueCard/ContextualDropdown.tsx",
    "content": "import cx from 'classnames';\nimport ClipboardJS from 'clipboard';\nimport { inject, observer } from 'mobx-react';\nimport React, { useEffect } from 'react';\nimport { compose } from 'recompose';\nimport styled from 'styled-components';\n\nimport { toast } from '../../../../components/ToastManager/ToastManager';\nimport { SettingsStore } from '../../../../store/settings';\nimport { IIssue } from './IssueCard';\n\nconst Wrapper = styled.div`\n  .overlay {\n    display: none;\n  }\n\n  :hover {\n    .overlay {\n      display: flex;\n    }\n  }\n`;\n\nconst Overlay = styled.div<{ dark: boolean }>`\n  top: 16px;\n  left: 2px;\n\n  :before {\n    content: '';\n    height: 0px;\n    width: 1px;\n    border-bottom: 4px solid ${({ dark }) => (dark ? '#606f7b' : 'white')};\n    border-left: 4px solid transparent;\n    border-right: 4px solid transparent;\n    position: absolute;\n    top: -4px;\n    left: 6px;\n  }\n`;\n\nconst makeActions = (issue: IIssue) => {\n  const type = issue.pull_request ? 'Pull request' : 'Issue';\n\n  return [\n    {\n      label: 'Copy Link',\n      'data-clipboard-text': issue.html_url,\n      onClick: () => toast(`${type} link was copied to the clipboard`, 'info'),\n    },\n    {\n      label: 'Copy Title',\n      'data-clipboard-text': issue.title,\n      onClick: () => toast(`${type} title was copied to the clipboard`, 'info'),\n    },\n  ];\n};\n\ninterface IProps {\n  issue: IIssue;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const ContextualDropdown = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ issue, settingsStore }) => {\n  useEffect(() => {\n    const clipboard = new ClipboardJS('[data-clipboard-text]');\n    return () => clipboard.destroy();\n  });\n\n  const actions = makeActions(issue);\n\n  return (\n    <Wrapper className=\"inline-block relative\">\n      <i className=\"fa fa-caret-down py-1 px-2 -mt-1 cursor-pointer\" />\n      <Overlay\n        dark={settingsStore.isDark}\n        className={cx([\n          'overlay',\n          'absolute py-1',\n          settingsStore.isDark\n            ? 'bg-gray-700'\n            : 'bg-white border border-gray-200',\n          'whitespace-nowrap rounded shadow-lg',\n          'flex flex-col',\n        ])}\n      >\n        {actions.map(({ label, ...otherProps }) => (\n          <div\n            className=\"text-sm px-4 py-1 cursor-pointer hover:underline\"\n            key={label}\n            {...otherProps}\n          >\n            {label}\n          </div>\n        ))}\n      </Overlay>\n    </Wrapper>\n  );\n});\n"
  },
  {
    "path": "src/providers/github/components/IssueCard/IssueCard.tsx",
    "content": "import cx from 'classnames';\nimport { get } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\nimport * as timeago from 'timeago.js';\n\nimport { SettingsStore } from '../../../../store/settings';\nimport { CheckStatusIndicator } from './CheckStatusIndicator';\nimport { ConflictIndicator } from './ConflictIndicator';\nimport { ContextualDropdown } from './ContextualDropdown';\nimport { IssueStatusIndicator } from './IssueStatusIndicator';\nimport { LabelBadge } from './LabelBadge';\nimport { IssueStatus, TimelineItem, TimelineItemType } from './types';\n\nexport interface IIssue {\n  type: 'PullRequest' | 'Issue';\n  title: string;\n  url: string;\n  html_url: string;\n  state: 'open' | 'closed' | 'merged';\n  isDraft: boolean;\n  status: IssueStatus;\n  number: number;\n  createdAt: string;\n  conflicting: boolean;\n  pull_request?: {\n    url: string;\n  };\n  author: {\n    url: string;\n    login: string;\n    avatarUrl: string;\n  };\n  repository: {\n    url: string;\n    nameWithOwner: string;\n  };\n  reviews?: {\n    totalCount: number;\n  };\n  comments?: {\n    totalCount: number;\n  };\n  labels: Array<{ color: string; name: string }>;\n  timelineItems?: TimelineItem[];\n}\n\ninterface IProps {\n  data: IIssue;\n  isNew: boolean;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const IssueCard = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ data: issue, isNew, settingsStore }) => {\n  const linkStyle = settingsStore.isDark\n    ? 'text-blue-400'\n    : 'text-blue-500 hover:text-blue-600';\n\n  const firstTimelineItem = get(issue, 'timelineItems.nodes.0');\n\n  function getTotalCommentsCount() {\n    return (\n      get(issue, 'reviews.totalCount', 0) + get(issue, 'comments.totalCount', 0)\n    );\n  }\n\n  function getLastActivityDate(item: TimelineItem) {\n    switch (item.__typename) {\n      case TimelineItemType.ISSUE_COMMENT:\n        return item.createdAt;\n      case TimelineItemType.PULL_REQUEST_COMMIT:\n        return item.commit.committedDate;\n      case TimelineItemType.PULL_REQUEST_REVIEW:\n        return item.createdAt;\n    }\n  }\n\n  return (\n    <div\n      className={cx(\n        'p-6 flex border-l-4',\n        isNew ? 'border-blue-500' : 'border-transparent'\n      )}\n    >\n      <div className=\"flex items-center justify-center flex-shrink-0 pr-4\">\n        <div\n          className={cx(\n            'w-16 h-16 rounded-full overflow-hidden',\n            settingsStore.isDark ? 'bg-gray-700' : 'bg-gray-400'\n          )}\n        >\n          <img src={issue.author.avatarUrl} />\n        </div>\n      </div>\n      <div className=\"flex-1 min-w-0\">\n        <div className=\"flex-1 flex justify-between items-center\">\n          <div className=\"flex-1 flex items-center mb-1 min-w-0\">\n            <IssueStatusIndicator issue={issue} />\n\n            <span className={cx('truncate pb-1 min-w-0', linkStyle)}>\n              <a\n                className={cx('hover:underline', linkStyle)}\n                href={issue.repository.url}\n              >\n                {issue.repository.nameWithOwner}\n              </a>\n              <span className=\"mx-1\">•</span>\n              <a\n                className={cx('text-lg hover:underline', linkStyle)}\n                href={issue.url}\n                title={issue.title}\n              >\n                {issue.title}{' '}\n              </a>\n            </span>\n\n            <CheckStatusIndicator status={issue.status} />\n\n            <ConflictIndicator conflicting={issue.conflicting} />\n          </div>\n          <a\n            href={issue.url}\n            className=\"ml-2 text-gray-600 hover:text-gray-500\"\n          >\n            <i className=\"far fa-comment-alt\" /> {getTotalCommentsCount()}\n          </a>\n        </div>\n        <div\n          className={cx(\n            'text-xs',\n            settingsStore.isDark ? 'text-gray-500' : 'text-gray-700'\n          )}\n        >\n          #{issue.number} opened {timeago.format(issue.createdAt)} by{' '}\n          <a\n            href={issue.author.url}\n            className={cx(\n              'no-underline hover:underline',\n              settingsStore.isDark ? 'text-gray-500' : 'text-gray-700'\n            )}\n          >\n            {issue.author.login}\n          </a>\n          {firstTimelineItem && (\n            <React.Fragment>\n              {', '}\n              <span>\n                last activity{' '}\n                {timeago.format(getLastActivityDate(firstTimelineItem))}\n              </span>\n            </React.Fragment>\n          )}\n          <ContextualDropdown issue={issue} />\n        </div>\n        <div className=\"flex flex-wrap gap-y-1 gap-x-1 mt-3\">\n          {issue.labels.map(label => (\n            <LabelBadge key={label.name} label={label} />\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/providers/github/components/IssueCard/IssueStatusIndicator.tsx",
    "content": "import cx from 'classnames';\nimport React from 'react';\n\nimport { IIssue } from './IssueCard';\nimport {assertUnreachable} from \"../../../../lib/assertUnreachable\";\n\ninterface IProps {\n  issue: Pick<IIssue, 'state' | 'isDraft' | 'type'>;\n}\n\nexport const IssueStatusIndicator = ({ issue }: IProps) => (\n  <i\n    className={cx(\n      'mr-2',\n      issue.type === 'PullRequest'\n        ? 'fas fa-code-branch'\n        : 'fas fa-exclamation-circle',\n      getColor(issue.state.toLowerCase() as IIssue['state'], issue.isDraft)\n    )}\n  />\n);\n\nfunction getColor(state: IIssue['state'], isDraft: boolean)\n{\n  switch (state) {\n    case 'open':\n      return isDraft ? 'text-gray-500' : 'text-green-500';\n    case 'closed':\n      return 'text-red-500';\n    case 'merged':\n      return 'text-purple-500';\n    default:\n      return assertUnreachable(state, 'text-gray-500');\n  }\n}\n"
  },
  {
    "path": "src/providers/github/components/IssueCard/LabelBadge.tsx",
    "content": "import cx from 'classnames';\nimport contrast from 'contrast';\nimport React from 'react';\n\ninterface IProps {\n  label: {\n    name: string;\n    color: string;\n  };\n}\n\nexport const LabelBadge = ({ label }: IProps) => {\n  const background = `#${label.color}`;\n\n  const isDark = contrast(background) === 'dark';\n\n  return (\n    <div\n      className={cx(\n        'text-xs whitespace-nowrap py-1 px-2 rounded',\n        isDark ? 'text-white' : 'text-gray-900'\n      )}\n      style={{ background }}\n    >\n      {label.name}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/providers/github/components/IssueCard/index.ts",
    "content": "export { IssueCard } from './IssueCard';\n"
  },
  {
    "path": "src/providers/github/components/IssueCard/types.ts",
    "content": "export enum IssueStatus {\n  PENDING = 'PENDING',\n  SUCCESS = 'SUCCESS',\n  FAILURE = 'FAILURE',\n  UNKNOWN = 'UNKNOWN',\n}\n\nexport enum TimelineItemType {\n  ISSUE_COMMENT = 'IssueComment',\n  PULL_REQUEST_COMMIT = 'PullRequestCommit',\n  PULL_REQUEST_REVIEW = 'PullRequestReview',\n}\n\nexport type TimelineItem =\n  | IIssueComment\n  | IPullRequestCommit\n  | IPullRequestReview;\n\ninterface IIssueComment {\n  __typename: TimelineItemType.ISSUE_COMMENT;\n  createdAt: string;\n}\n\ninterface IPullRequestCommit {\n  __typename: TimelineItemType.PULL_REQUEST_COMMIT;\n  commit: {\n    committedDate: string;\n  };\n}\n\ninterface IPullRequestReview {\n  __typename: TimelineItemType.PULL_REQUEST_REVIEW;\n  createdAt: string;\n}\n"
  },
  {
    "path": "src/providers/github/components/ProfileCard.tsx",
    "content": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport { IGithubProfile } from '../index';\nimport { SettingsStore } from '../../../store/settings';\n\ninterface IProps {\n  profile: IGithubProfile;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const ProfileCard = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ profile, settingsStore }) => {\n  if (!profile) {\n    return null;\n  }\n\n  return (\n    <div className=\"flex mb-8\">\n      <div\n        className={cx(\n          'w-16 h-16 rounded-full overflow-hidden mr-3',\n          settingsStore.isDark ? 'bg-gray-700' : 'bg-gray-400'\n        )}\n      >\n        <img src={profile.avatar_url} />\n      </div>\n      <div className=\"pt-2\">\n        <div\n          className={settingsStore.isDark ? 'text-gray-300' : 'text-gray-800'}\n        >\n          {profile.name}\n        </div>\n        <a\n          href={profile.html_url}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"text-sm text-gray-600 mt-1\"\n        >\n          @{profile.login}\n        </a>\n      </div>\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/providers/github/components/Settings.tsx",
    "content": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React, { useState } from 'react';\nimport { compose } from 'recompose';\nimport styled from 'styled-components';\n\nimport { GithubProvider } from '..';\nimport { Button, ButtonType } from '../../../components/Button';\nimport { toast } from '../../../components/ToastManager';\nimport { SettingsStore } from '../../../store/settings';\nimport { ProfileCard } from './ProfileCard';\n\nconst CREATE_TOKEN_URL =\n  'https://github.com/settings/tokens/new?scopes=repo&description=octolenses';\n\nconst Input = styled.input<{ dark?: boolean }>`\n  ::placeholder {\n    color: ${({ dark }) => (dark ? '#8795a1' : '#b8c2cc')};\n    opacity: 1;\n  }\n`;\n\ninterface IProps {\n  provider: GithubProvider;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const Settings = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ provider, settingsStore }) => {\n  const [token, setToken] = useState(provider.settings.token || '');\n\n  function handleSubmit() {\n    provider.setToken(token);\n    toast('Token was saved', 'info');\n  }\n\n  return (\n    <div className=\"flex-1 flex flex-col items-stretch\">\n      <ProfileCard profile={provider.profile} />\n      <div className=\"font-medium\">Github Personal Access Token</div>\n      <div className=\"mt-4 leading-normal\">\n        <p>\n          You can generate a Personal Access Token on{' '}\n          <a\n            className=\"text-blue-500\"\n            href={CREATE_TOKEN_URL}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            this page\n          </a>\n          .<br />\n          It needs to have the following scope:{' '}\n          <span\n            className={cx(\n              'font-mono px-2 rounded',\n              settingsStore.isDark ? 'bg-gray-800' : 'bg-gray-100'\n            )}\n          >\n            repo\n          </span>\n        </p>\n      </div>\n      <div className=\"relative flex items-center mt-4\">\n        <Input\n          id=\"token\"\n          type=\"password\"\n          value={token}\n          onChange={(event: any) => setToken(event.target.value)}\n          placeholder=\"xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx\"\n          dark={settingsStore.isDark}\n          className={cx(\n            'w-full rounded outline-none pl-10 pr-3 py-2 text-gray-600 tracking-wider font-mono',\n            settingsStore.isDark ? 'bg-gray-800' : 'bg-gray-100'\n          )}\n        />\n        <i\n          className={cx(\n            'fas fa-key absolute left-0 ml-3',\n            settingsStore.isDark ? 'text-gray-600' : 'text-gray-500'\n          )}\n        />\n      </div>\n      <div className=\"mt-8 flex justify-end\">\n        <Button onClick={handleSubmit} type={ButtonType.PRIMARY}>\n          Save\n        </Button>\n      </div>\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/providers/github/fetchers/client.ts",
    "content": "import { get, pickBy } from 'lodash';\n\nimport {\n  InvalidCredentials,\n  NeedTokenError,\n  RateLimitError,\n} from '../../../errors';\n\ninterface IClientParams {\n  endpoint: string;\n  token?: string;\n  qs?: string;\n  body?: string;\n  method?: 'GET' | 'POST';\n}\n\n/**\n * Fetch data from GitHub API\n */\nexport const client = async ({\n  endpoint,\n  token,\n  qs,\n  body,\n  method = 'GET',\n}: IClientParams) => {\n  const url = `https://api.github.com${endpoint}?${qs || ''}`;\n\n  const response = await fetch(url, {\n    body: method === 'POST' ? body : undefined,\n    method,\n    headers: pickBy({\n      'User-Agent': 'OctoLenses Github Dashboard',\n      Authorization: token && `Bearer ${token}`,\n      Accept: 'application/vnd.github.antiope-preview+json', // Allow access to Previews API\n    }),\n  });\n\n  if (!response.ok) {\n    await handleErrorResponse(response);\n  }\n\n  return await response.json();\n};\n\n/**\n * Handle an error response from GitHub API\n */\nconst handleErrorResponse = async (response: Response) => {\n  const status = response.status;\n  const { message = '', errors = [] } = await response.json();\n\n  const firstErrorMessage = get(errors, '0.message', '');\n\n  if (status === 401 && message.includes('Bad credentials')) {\n    throw new InvalidCredentials();\n  }\n\n  if (status === 403 && message.includes('API rate limit')) {\n    const rateLimitReset = response.headers.get('X-RateLimit-Reset');\n    const remainingRateLimit = Number(rateLimitReset) - Date.now() / 1000;\n    throw new RateLimitError(remainingRateLimit);\n  }\n\n  if (\n    status === 422 &&\n    firstErrorMessage.includes('do not exist or you do not have permission')\n  ) {\n    throw new NeedTokenError();\n  }\n};\n"
  },
  {
    "path": "src/providers/github/fetchers/graphql/query.ts",
    "content": "const commonFields = `\n  url\n  title\n  state\n  number\n  createdAt\n  author {\n    url\n    login\n    avatarUrl\n  }\n  labels(first: 10) {\n    edges {\n      node {\n        name\n        color\n      }\n    }\n  }\n  repository {\n    nameWithOwner\n    url\n  }\n  comments {\n    totalCount\n  }\n`;\n\nconst issueFragment = `\n  fragment IssueFragment on Issue {\n    ${commonFields}\n  }`;\n\nconst pullRequestFragment = `\n  fragment PullRequestFragment on PullRequest {\n    ${commonFields}\n    mergeable\n    isDraft\n    reviews {\n      totalCount\n    }\n    commits(last: 1) {\n      nodes {\n        commit {\n          status {\n            state\n          }\n          checkSuites(first: 1) {\n            nodes {\n              status\n              conclusion\n              app {\n                name\n              }\n            }\n          }\n        }\n      }\n    }\n    timelineItems(itemTypes: [PULL_REQUEST_COMMIT, PULL_REQUEST_REVIEW, ISSUE_COMMENT], last: 1) {\n      nodes {\n        __typename\n        ... on IssueComment {\n          createdAt\n        }\n        ... on PullRequestReview {\n          createdAt\n        }\n        ... on PullRequestCommit {\n          commit {\n            committedDate\n          }\n        }\n      }\n    }\n  }`;\n\nexport const makeQuery = (filterString: string) => `\n  ${pullRequestFragment}\n  ${issueFragment}\n  query results {\n    search(query: \"${filterString}\", type: ISSUE, first: 100) {\n      edges {\n        node {\n          __typename\n          ... on PullRequest {\n            ...PullRequestFragment\n          }\n          ... on Issue {\n            ...IssueFragment\n          }\n        }\n      }\n    }\n  }`;\n"
  },
  {
    "path": "src/providers/github/fetchers/graphql/search.ts",
    "content": "import { chain, omit, map } from 'lodash';\n\nimport { Cache } from '../../../../lib/cache';\nimport { Filter } from '../../../../store/filters';\nimport { client } from '../client';\nimport { makeQuery } from './query';\nimport {\n  extractConflictStatus,\n  extractGraphqlLabels,\n  extractGraphqlStatus,\n} from './utils';\n\n/**\n * Fetch a filter using the shiny GraphQL API\n * @param {object} options.filter\n */\nexport const search = async (filter: Filter, token: string) => {\n  const filterString = chain(filter.predicates)\n    .map(predicate => filter.serializePredicate(predicate))\n    .join(' ')\n    .replace(/\"/g, '\\\\\"')\n    .value();\n\n  const cacheKey = `github.graphql.${filter.hash}`;\n\n  const response = await Cache.remember(cacheKey, 60, async () =>\n    client({\n      endpoint: '/graphql',\n      method: 'POST',\n      body: JSON.stringify({ query: makeQuery(filterString) }),\n      token,\n    })\n  );\n\n  return formatResponse(response);\n};\n\n/**\n * Format a graphql response so that it's easy to use\n */\nexport const formatResponse = (response: any) => {\n  const issues = map(response.data.search.edges, 'node');\n  return issues.map(issue => ({\n    ...omit(issue, ['commits', '__typename', 'mergeable']),\n    type: issue.__typename,\n    status: extractGraphqlStatus(issue),\n    labels: extractGraphqlLabels(issue),\n    conflicting: extractConflictStatus(issue),\n  }));\n};\n"
  },
  {
    "path": "src/providers/github/fetchers/graphql/utils.ts",
    "content": "import { get, map } from 'lodash';\n\nimport { IssueStatus } from '../../components/IssueCard/types';\n\nexport const COMMIT_STATUS_TO_STATUS: Record<any, IssueStatus> = {\n  EXPECTED: IssueStatus.UNKNOWN,\n  ERROR: IssueStatus.FAILURE,\n  FAILURE: IssueStatus.FAILURE,\n  PENDING: IssueStatus.PENDING,\n  SUCCESS: IssueStatus.SUCCESS,\n};\n\nexport const CHECK_CONCLUSION_TO_STATUS: Record<any, IssueStatus> = {\n  ACTION_REQUIRED: IssueStatus.UNKNOWN,\n  TIMED_OUT: IssueStatus.UNKNOWN,\n  CANCELLED: IssueStatus.UNKNOWN,\n  FAILURE: IssueStatus.FAILURE,\n  SUCCESS: IssueStatus.SUCCESS,\n  NEUTRAL: IssueStatus.UNKNOWN,\n};\n\nexport function extractGraphqlStatus(issue: any) {\n  const commitStatus = get(issue, 'commits.nodes.0.commit.status.state');\n  const checkStatus = get(issue, 'commits.nodes.0.commit.checkSuites.nodes.0');\n\n  // Old status API\n  if (commitStatus) {\n    return COMMIT_STATUS_TO_STATUS[commitStatus];\n  }\n\n  // New check status API\n  if (checkStatus) {\n    const { status, conclusion } = checkStatus;\n\n    if (status !== 'COMPLETED') {\n      return IssueStatus.PENDING;\n    }\n\n    return CHECK_CONCLUSION_TO_STATUS[conclusion];\n  }\n\n  return IssueStatus.UNKNOWN;\n}\n\nexport function extractGraphqlLabels(issue: any) {\n  return map(issue.labels.edges, 'node');\n}\n\nexport function extractConflictStatus(issue: any) {\n  return issue.mergeable === 'CONFLICTING';\n}\n"
  },
  {
    "path": "src/providers/github/fetchers/index.ts",
    "content": "import { IGithubSettings } from '..';\nimport { Filter } from '../../../store/filters';\nimport { search as graphqlSearch } from './graphql/search';\nimport { search as restSearch } from './rest/search';\n\nexport const fetchFilter = async (\n  filter: Filter,\n  settings: IGithubSettings\n) => {\n  if (settings.token) {\n    return graphqlSearch(filter, settings.token);\n  }\n\n  return restSearch(filter);\n};\n"
  },
  {
    "path": "src/providers/github/fetchers/rest/profile.ts",
    "content": "import { client } from '../client';\n\nexport const fetchProfile = async (token: string) => {\n  return client({ endpoint: '/user', token });\n};\n"
  },
  {
    "path": "src/providers/github/fetchers/rest/search.ts",
    "content": "import { chain, map, pick } from 'lodash';\n\nimport { Cache } from '../../../../lib/cache';\nimport { Filter } from '../../../../store/filters';\nimport { IssueStatus } from '../../components/IssueCard/types';\nimport { client } from '../client';\n\n/**\n * Fetch a filter on the old REST API. This is only supposed to be\n * used when the user has not set a token. This won't return all the\n * value we need (for example there are no build status).\n * @param {object} options.filter\n */\nexport const search = async (filter: Filter) => {\n  const filterString = chain(filter.predicates)\n    .map(predicate => filter.serializePredicate(predicate))\n    .map(encodeURIComponent)\n    .join('+')\n    .value();\n\n  const cacheKey = `github.rest.${filter.hash}`;\n\n  const { items: issues = [] } = await Cache.remember(cacheKey, 60, async () =>\n    client({\n      endpoint: '/search/issues',\n      qs: `per_page=100&q=${filterString}`,\n    })\n  );\n\n  return formatResponse(issues);\n};\n\n/**\n * Format a REST response so that it's compatible with the GraphQL one\n */\nexport const formatResponse = (response: any) => {\n  return map(response, issue => ({\n    url: issue.html_url,\n    title: issue.title,\n    number: issue.number,\n    state: issue.state,\n    createdAt: issue.created_at,\n    author: {\n      url: issue.user.html_url,\n      login: issue.user.login,\n      avatarUrl: issue.user.avatar_url,\n    },\n    repository: {\n      nameWithOwner: parseRestRepoName(issue.repository_url),\n      url: parseRestRepoUrl(issue.repository_url),\n    },\n    labels: extractRestLabels(issue),\n    type: issue.pull_request ? 'PullRequest' : 'Issue',\n    comments: { totalCount: issue.comments },\n    reviews: { totalCount: 0 },\n    status: IssueStatus.UNKNOWN,\n  }));\n};\n\nconst extractRestLabels = (issue: any) =>\n  map(issue.labels, label => pick(label, ['color', 'name']));\n\nconst parseRestRepoName = (url: string) =>\n  chain(url)\n    .split('/')\n    .slice(-2)\n    .join('/')\n    .value();\n\nconst parseRestRepoUrl = (apiUrl: string) =>\n  apiUrl.replace('api.github.com/repos/', 'github.com/');\n"
  },
  {
    "path": "src/providers/github/index.tsx",
    "content": "import { find } from 'lodash';\nimport { action, observable } from 'mobx';\nimport React from 'react';\n\nimport { Filter } from '../../store/filters';\nimport { AbstractProvider } from '../AbstractProvider';\nimport { IssueCard } from './components/IssueCard';\nimport { Settings } from './components/Settings';\nimport { fetchFilter } from './fetchers';\nimport { fetchProfile } from './fetchers/rest/profile';\nimport { availablePredicates } from './predicates';\n\nexport interface IGithubSettings {\n  token: string;\n}\n\nexport interface IGithubProfile {\n  login: string;\n  name: string;\n  avatar_url: string;\n  html_url: string;\n}\n\nexport class GithubProvider extends AbstractProvider<IGithubSettings> {\n  public id = 'github';\n  public label = 'GitHub';\n  public settingsComponent = () => <Settings provider={this} />;\n  public cardComponent = IssueCard;\n\n  @observable\n  public profile: IGithubProfile = null;\n\n  @action.bound\n  public async initialize() {\n    await this.fetchProfile();\n  }\n\n  public async fetchFilter(filter: Filter) {\n    return fetchFilter(filter, this.settings);\n  }\n\n  public resolveFilterItemIdentifier(item: any) {\n    return item.number;\n  }\n\n  public getAvailablePredicates = () => availablePredicates;\n\n  public findPredicate(name: string) {\n    return find(this.getAvailablePredicates(), { name });\n  }\n\n  @action.bound\n  public async fetchProfile() {\n    if (!this.settings.token) {\n      return;\n    }\n\n    this.profile = await fetchProfile(this.settings.token);\n  }\n\n  @action.bound\n  public setToken(token: string) {\n    this.settings.token = token;\n\n    if (token) {\n      this.fetchProfile();\n    } else {\n      this.profile = null;\n    }\n  }\n}\n\nexport const github = new GithubProvider();\n"
  },
  {
    "path": "src/providers/github/predicates/createdOrUpdatedAt.ts",
    "content": "import moment from 'moment';\nimport { IDropdownPredicate, PredicateType } from '../../types';\n\nenum Preset {\n  ONE_HOUR_AGO = 'one_hour_ago',\n  ONE_DAY_AGO = 'one_day_ago',\n  ONE_WEEK_AGO = 'one_week_ago',\n  ONE_MONTH_AGO = 'one_month_ago',\n}\n\nenum Operator {\n  MORE_THAN = 'more_than',\n  LESS_THAN = 'less_than',\n}\n\nfunction makeDatePredicate(name: string, label: string, githubField: string): IDropdownPredicate {\n  return {\n    name,\n    label,\n    type: PredicateType.DROPDOWN,\n    operators: [\n      { value: Operator.MORE_THAN, label: 'More than' },\n      { value: Operator.LESS_THAN, label: 'Less than' },\n    ],\n    choices: [\n      { value: Preset.ONE_HOUR_AGO, label: '1 hour ago' },\n      { value: Preset.ONE_DAY_AGO, label: '1 day ago' },\n      { value: Preset.ONE_WEEK_AGO, label: '1 week ago' },\n      { value: Preset.ONE_MONTH_AGO, label: '1 month ago' },\n    ],\n    serialize: ({ value, operator }) => {\n      let date = null;\n      switch (value) {\n        // Less than\n        case Preset.ONE_HOUR_AGO:\n          date = moment().subtract(1, 'hour');\n          break;\n        case Preset.ONE_DAY_AGO:\n          date = moment().subtract(1, 'day');\n          break;\n        case Preset.ONE_WEEK_AGO:\n          date = moment().subtract(1, 'week');\n          break;\n        case Preset.ONE_MONTH_AGO:\n          date = moment().subtract(1, 'month');\n          break;\n      }\n\n      let operatorSymbol = null;\n      switch (operator) {\n        case Operator.LESS_THAN:\n          operatorSymbol = '>';\n          break;\n        case Operator.MORE_THAN:\n          operatorSymbol = '<';\n          break;\n      }\n\n      if (date === null || operator === null)\n        return null;\n\n      return `${githubField}:${operatorSymbol}${date.format('YYYY-MM-DD')}`;\n    },\n  };\n}\n\nexport const createdAt: IDropdownPredicate = makeDatePredicate('created at', 'Created At', 'created');\nexport const updatedAt: IDropdownPredicate = makeDatePredicate('updated at', 'Updated At', 'updated');"
  },
  {
    "path": "src/providers/github/predicates/draft.ts",
    "content": "import { IDropdownPredicate, PredicateType } from '../../types';\n\nexport const draft: IDropdownPredicate = {\n  name: 'draft',\n  label: 'Is Draft?',\n  type: PredicateType.DROPDOWN,\n  choices: [\n    { value: 'false', label: 'No' },\n    { value: 'true', label: 'Yes' },\n  ],\n  operators: [],\n  serialize: ({ value }) => `draft:${value}`,\n};\n"
  },
  {
    "path": "src/providers/github/predicates/index.ts",
    "content": "import { capitalize } from 'lodash';\n\nimport { Predicate, PredicateType } from '../../types';\nimport { mergeStatus } from './mergeStatus';\nimport { review } from './review';\nimport { status } from './status';\nimport { type } from './type';\nimport { draft } from \"./draft\";\nimport { createdAt, updatedAt } from './createdOrUpdatedAt';\n\nenum GithubOperators {\n  EQUAL = 'equal',\n  NOT_EQUAL = 'not_equal',\n}\n\ninterface ISimplePredicatePayload {\n  name: string;\n  placeholder: string;\n  label?: string;\n}\n\n/**\n * Makes a simple text predicate\n * @param options Options configuring the predicate\n */\nexport const makeSimplePredicate = ({\n  name,\n  label,\n  placeholder,\n}: ISimplePredicatePayload): Predicate => ({\n  name,\n  label: label || capitalize(name),\n  placeholder,\n  type: PredicateType.TEXT,\n  operators: [\n    { value: GithubOperators.EQUAL, label: '=' },\n    { value: GithubOperators.NOT_EQUAL, label: '!=' },\n  ],\n  serialize: ({ value, operator }) => {\n    const modifier = operator === GithubOperators.NOT_EQUAL ? '-' : '';\n    return `${modifier}${name}:\"${value}\"`;\n  },\n});\n\nexport const availablePredicates: Predicate[] = [\n  makeSimplePredicate({\n    name: 'assignee',\n    label: 'Assignee',\n    placeholder: 'USERNAME',\n  }),\n  makeSimplePredicate({\n    name: 'author',\n    label: 'Author',\n    placeholder: 'USERNAME',\n  }),\n  makeSimplePredicate({\n    name: 'label',\n    label: 'Label',\n    placeholder: 'LABEL',\n  }),\n  makeSimplePredicate({\n    name: 'project',\n    label: 'Project',\n    placeholder: 'USERNAME/REPOSITORY/PROJECT',\n  }),\n  makeSimplePredicate({\n    name: 'mentions',\n    label: 'Mentions',\n    placeholder: 'USERNAME',\n  }),\n  makeSimplePredicate({\n    name: 'team',\n    label: 'Team',\n    placeholder: 'ORGNAME/TEAMNAME',\n  }),\n  makeSimplePredicate({\n    name: 'commenter',\n    label: 'Commenter',\n    placeholder: 'USERNAME',\n  }),\n  makeSimplePredicate({\n    name: 'involves',\n    label: 'Involves',\n    placeholder: 'USERNAME',\n  }),\n  makeSimplePredicate({\n    name: 'milestone',\n    label: 'Milestone',\n    placeholder: 'MILESTONE',\n  }),\n  makeSimplePredicate({\n    name: 'user',\n    label: 'User',\n    placeholder: 'USERNAME',\n  }),\n  makeSimplePredicate({\n    name: 'repo',\n    label: 'Repository',\n    placeholder: 'USERNAME/REPOSITORY',\n  }),\n  makeSimplePredicate({\n    name: 'org',\n    label: 'Organization',\n    placeholder: 'ORGNAME',\n  }),\n  makeSimplePredicate({\n    name: 'reviewed-by',\n    label: 'Reviewed by',\n    placeholder: 'USERNAME',\n  }),\n  makeSimplePredicate({\n    name: 'review-requested',\n    label: 'Requested Reviewer (User)',\n    placeholder: 'USERNAME',\n  }),\n  makeSimplePredicate({\n    name: 'team-review-requested',\n    label: 'Requested Reviewer (Team)',\n    placeholder: 'ORGNAME/TEAMNAME',\n  }),\n  type,\n  status,\n  mergeStatus,\n  review,\n  draft,\n  createdAt,\n  updatedAt\n];\n"
  },
  {
    "path": "src/providers/github/predicates/mergeStatus.ts",
    "content": "import { IDropdownPredicate, PredicateType } from '../../types';\n\nexport const mergeStatus: IDropdownPredicate = {\n  name: 'merge status',\n  label: 'Merge Status',\n  type: PredicateType.DROPDOWN,\n  choices: [\n    { value: 'merged', label: 'Merged' },\n    { value: 'unmerged', label: 'Unmerged' },\n  ],\n  operators: [],\n  serialize: ({ value }) => `is:${value}`,\n};\n"
  },
  {
    "path": "src/providers/github/predicates/review.ts",
    "content": "import { IDropdownPredicate, PredicateType } from '../../types';\n\nexport const review: IDropdownPredicate = {\n  name: 'review',\n  label: 'Review Status',\n  type: PredicateType.DROPDOWN,\n  choices: [\n    { value: 'none', label: 'None' },\n    { value: 'required', label: 'Required' },\n    { value: 'approved', label: 'Approved' },\n    { value: 'changes_requested', label: 'Changes Requested' },\n  ],\n  operators: [],\n  serialize: ({ value }) => `review:${value}`,\n};\n"
  },
  {
    "path": "src/providers/github/predicates/status.ts",
    "content": "import { IDropdownPredicate, PredicateType } from '../../types';\n\nexport const status: IDropdownPredicate = {\n  name: 'status',\n  label: 'Status',\n  type: PredicateType.DROPDOWN,\n  choices: [\n    { value: 'open', label: 'Open' },\n    { value: 'closed', label: 'Closed' },\n  ],\n  operators: [],\n  serialize: ({ value }) => `is:${value}`,\n};\n"
  },
  {
    "path": "src/providers/github/predicates/type.ts",
    "content": "import { IDropdownPredicate, PredicateType } from '../../types';\n\nexport const type: IDropdownPredicate = {\n  name: 'type',\n  label: 'Type',\n  type: PredicateType.DROPDOWN,\n  choices: [\n    { value: 'pr', label: 'PRs' },\n    { value: 'issue', label: 'Issues' },\n  ],\n  operators: [],\n  serialize: ({ value }) => `type:${value}`,\n};\n"
  },
  {
    "path": "src/providers/index.ts",
    "content": "import { github } from './github';\nimport { jira } from './jira';\nimport { ProviderType } from './types';\n\nexport { AbstractProvider } from './AbstractProvider';\nexport * from './types';\n\nexport const providers = {\n  [ProviderType.GITHUB]: github,\n  [ProviderType.JIRA]: jira,\n};\n"
  },
  {
    "path": "src/providers/jira/components/AvailableResources.tsx",
    "content": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport { SettingsStore } from '../../../store/settings';\nimport { IJiraResource } from '../index';\n\ninterface IProps {\n  resources: IJiraResource[];\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const AvailableResources = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ resources, settingsStore }) => {\n  if (!resources || !resources.length) {\n    return null;\n  }\n\n  return (\n    <div className=\"mt-8\">\n      <div className=\"font-medium mb-3\">Available resources</div>\n\n      {resources.map((resource, index) => (\n        <div key={index} className=\"flex mb-8\">\n          <div\n            className={cx(\n              'w-16 h-16 rounded-full overflow-hidden mr-3',\n              settingsStore.isDark ? 'bg-gray-700' : 'bg-gray-400'\n            )}\n          >\n            <img src={resource.avatarUrl} />\n          </div>\n          <div className=\"pt-2\">\n            <div\n              className={\n                settingsStore.isDark ? 'text-gray-300' : 'text-gray-800'\n              }\n            >\n              {resource.name}\n            </div>\n            <div className=\"text-sm text-gray-600 mt-1\">{resource.id}</div>\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/providers/jira/components/IssueCard/IssueCard.tsx",
    "content": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\nimport * as timeago from 'timeago.js';\n\nimport { SettingsStore } from '../../../../store/settings';\nimport { JiraProvider } from '../../index';\nimport { StatusBadge } from './StatusBadge';\n\nexport interface IJiraIssue {\n  key: string;\n  fields: {\n    summary: string;\n    description: string;\n    created: string;\n    updated: string;\n    issuetype: {\n      name: string;\n      iconUrl: string;\n    };\n    project: {\n      name: string;\n      key: string;\n      avatarUrls: {\n        '48x48': string;\n      };\n    };\n    priority?: {\n      iconUrl: string;\n      name: string;\n    };\n    assignee: {\n      displayName: string;\n      emailAddress: string;\n      avatarUrls: {\n        '48x48': string;\n      };\n    };\n    status: {\n      name: string;\n      statusCategory: {\n        colorName: string;\n      };\n    };\n  };\n}\n\nexport interface IProps {\n  provider: JiraProvider;\n  data: IJiraIssue;\n  isNew: boolean;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const IssueCard = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ data: issue, isNew, provider, settingsStore }) => {\n  // The API doesn't seem to return the URL to the actual issue on the web\n  // interface, so we use the one of the first resource. Note that for an\n  // account that has access to multiple resources, this will generate wrong\n  // URLs. Feel free to suggest a change if it doesn't work for you.\n  const url = `${provider.resources[0].url}/browse/${issue.key}`;\n\n  const linkStyle = settingsStore.isDark\n    ? 'text-blue-400'\n    : 'text-blue-500 hover:text-blue-600';\n\n  return (\n    <div\n      className={cx(\n        'p-6 flex border-l-4',\n        isNew ? 'border-blue-500' : 'border-transparent'\n      )}\n    >\n      <div className=\"flex items-center justify-center flex-shrink-0 pr-4\">\n        <div\n          className={cx(\n            'w-12 h-12 rounded-full overflow-hidden',\n            settingsStore.isDark ? 'bg-gray-700' : 'bg-gray-400'\n          )}\n        >\n          {issue.fields.assignee && (\n            <img src={issue.fields.assignee.avatarUrls['48x48']} />\n          )}\n        </div>\n      </div>\n      <div className=\"flex-1 min-w-0\">\n        <div className=\"flex-1 flex justify-between items-center\">\n          <div className=\"flex-1 flex items-center mb-1 min-w-0\">\n            <img src={issue.fields.issuetype.iconUrl} className=\"mr-1 mb-1\" />\n            {issue.fields.priority && (\n              <img\n                src={issue.fields.priority.iconUrl}\n                className=\"h-5 mr-1 mb-1\"\n              />\n            )}\n            <span className={cx('truncate pb-1 min-w-0', linkStyle)}>\n              <a\n                className={cx('text-lg hover:underline', linkStyle)}\n                href={url}\n                title={issue.fields.summary}\n              >\n                {issue.fields.summary}\n              </a>\n            </span>\n            <StatusBadge issue={issue} />\n          </div>\n        </div>\n        <div\n          className={cx(\n            'text-xs',\n            settingsStore.isDark ? 'text-gray-500' : 'text-gray-700'\n          )}\n        >\n          {issue.key} opened {timeago.format(issue.fields.created)}\n        </div>\n      </div>\n    </div>\n  );\n});\n"
  },
  {
    "path": "src/providers/jira/components/IssueCard/StatusBadge.tsx",
    "content": "import cx from 'classnames';\nimport React from 'react';\n\nimport { IJiraIssue } from './IssueCard';\n\ninterface IProps {\n  issue: IJiraIssue;\n}\n\ntype JiraColor = 'green' | 'yellow' | 'blue-gray';\n\nconst COLORS_TO_STYLE: Record<JiraColor, string> = {\n  green: 'text-green-500 bg-green-100',\n  yellow: 'text-yellow-800 bg-yellow-200',\n  'blue-gray': 'text-blue-600 bg-blue-100',\n};\n\nexport const StatusBadge = ({ issue }: IProps) => {\n  const name = issue.fields.status.name;\n  const colorName = issue.fields.status.statusCategory.colorName;\n\n  return (\n    <div\n      className={cx(\n        'rounded px-2 py-1 ml-3 whitespace-nowrap',\n        COLORS_TO_STYLE[colorName as JiraColor]\n      )}\n    >\n      {name}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/providers/jira/components/IssueCard/index.ts",
    "content": "export { IssueCard } from './IssueCard';\nexport type { IProps } from './IssueCard';\n"
  },
  {
    "path": "src/providers/jira/components/LoginButton.tsx",
    "content": "import cx from 'classnames';\nimport { chain } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport { JiraProvider } from '..';\nimport { Button, ButtonType } from '../../../components/Button';\nimport { SettingsStore } from '../../../store/settings';\nimport { ISwapResult, swapToken } from '../fetchers/swapToken';\n\nconst CLIENT_ID = '4WgiRI4XRQ2OTWof5i7yCKmlekkIldH0';\n\ninterface IProps {\n  provider: JiraProvider;\n}\n\ninterface IInnerProps extends IProps {\n  settingsStore: SettingsStore;\n}\n\nexport const LoginButton = compose<IInnerProps, IProps>(\n  inject('settingsStore'),\n  observer\n)(({ provider, settingsStore }) => {\n  async function handleLogin() {\n    try {\n      const data = await initJiraOauthFlow();\n      provider.setAuth(data);\n    } catch (error) {\n      console.error('Could not connect the Jira account', error);\n    }\n  }\n\n  return (\n    <>\n      <div className=\"font-medium mb-4\">Connect your Atlassian account</div>\n      <p>\n        Click on the button below to connect OctoLenses to your Atlassian\n        account.\n      </p>\n      <Button\n        onClick={handleLogin}\n        type={ButtonType.PRIMARY}\n        className=\"self-start mt-4\"\n      >\n        <i className=\"fas fa-sign-in-alt\" /> Connect your account\n      </Button>\n      <p\n        className={cx(\n          'text-base mt-4',\n          settingsStore.isDark ? 'text-gray-600' : 'text-gray-500'\n        )}\n      >\n        The only way to connect your Atlassian account is using the OAuth flow.\n        This means you’ll have to grant access to your account to the OctoLenses\n        Jira application.\n      </p>\n      <p\n        className={cx(\n          'text-base mt-4',\n          settingsStore.isDark ? 'text-gray-600' : 'text-gray-500'\n        )}\n      >\n        During the authentication flow, a token swap service (whose source code\n        is auditable{' '}\n        <a\n          href=\"https://github.com/rgehan/octolenses-jira-token-swap-service\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"text-blue\"\n        >\n          here\n        </a>\n        ) is used in order for you to obtain an access token.\n      </p>\n    </>\n  );\n});\n\n/**\n * Start an OAuth authentication flow with Jira.\n * It automatically handles the redirection_url as part of the chrome.identity\n * API. An external (privately hosted by me) token swap service is then used\n * to obtain access/refresh tokens.\n */\nasync function initJiraOauthFlow(): Promise<ISwapResult> {\n  const redirectUri = chrome.identity.getRedirectURL('provider_cb');\n  const redirectRegexp = new RegExp(redirectUri + '[#?](.*)');\n\n  return new Promise((resolve, reject) => {\n    const options = {\n      interactive: true,\n      url:\n        'https://auth.atlassian.com/authorize' +\n        '?audience=api.atlassian.com' +\n        `&client_id=${CLIENT_ID}` +\n        '&scope=read%3Ajira-user%20read%3Ajira-work%20offline_access' +\n        `&redirect_uri=${encodeURIComponent(redirectUri)}` +\n        `&response_type=code` +\n        `&prompt=consent`,\n    };\n\n    chrome.identity.launchWebAuthFlow(options, response => {\n      // A generic error happened\n      if (chrome.runtime.lastError) {\n        return reject(chrome.runtime.lastError.message);\n      }\n\n      // Unable to extract an authorization code from the response\n      const authCode = chain(redirectRegexp.exec(response))\n        .get(1)\n        .split('=')\n        .get(1)\n        .value();\n\n      if (!authCode) {\n        return reject(\n          `Couldn't extract a authorization code from Jira response`\n        );\n      }\n\n      // Swap the authorization code for an access and refresh token.\n      swapToken(authCode, redirectUri)\n        .then(data => {\n          resolve(data);\n        })\n        .catch(err => {\n          reject(err);\n        });\n    });\n  });\n}\n"
  },
  {
    "path": "src/providers/jira/components/LogoutButton.tsx",
    "content": "import { observer } from 'mobx-react';\nimport React from 'react';\n\nimport { JiraProvider } from '..';\nimport { Button, ButtonType } from '../../../components/Button';\n\ninterface IProps {\n  provider: JiraProvider;\n}\n\nexport const LogoutButton = observer(({ provider }: IProps) => {\n  function handleLogout() {\n    provider.disconnect();\n  }\n\n  return (\n    <>\n      <div className=\"font-medium mb-4\">Connect your Atlassian account</div>\n      <p>Your Atlassian account is properly connected to OctoLenses.</p>\n      <Button\n        onClick={handleLogout}\n        type={ButtonType.PRIMARY}\n        className=\"self-start mt-4\"\n      >\n        <i className=\"fas fa-sign-out-alt\" /> Disconnect your account\n      </Button>\n    </>\n  );\n});\n"
  },
  {
    "path": "src/providers/jira/components/Settings.tsx",
    "content": "import { observer } from 'mobx-react';\nimport React from 'react';\n\nimport { JiraProvider } from '..';\nimport { AvailableResources } from './AvailableResources';\nimport { LoginButton } from './LoginButton';\nimport { LogoutButton } from './LogoutButton';\n\ninterface IProps {\n  provider: JiraProvider;\n}\n\nexport const Settings = observer(({ provider }: IProps) => (\n  <div className=\"flex-1 flex flex-col items-stretch\">\n    {provider.settings.auth ? (\n      <LogoutButton provider={provider} />\n    ) : (\n      <LoginButton provider={provider} />\n    )}\n\n    <AvailableResources resources={provider.resources} />\n  </div>\n));\n"
  },
  {
    "path": "src/providers/jira/fetchers/index.ts",
    "content": "import { chain, get } from 'lodash';\nimport hash from 'object-hash';\n\nimport { IJiraResource, IJiraSettings } from '..';\nimport { Cache } from '../../../lib/cache';\nimport { Filter } from '../../../store/filters';\n\nexport async function fetchFilter(\n  filter: Filter,\n  settings: IJiraSettings,\n  resource: IJiraResource\n): Promise<any[]> {\n  const site = resource.id;\n  const token = get(settings, 'auth.access_token');\n\n  const filterString = chain(filter.predicates)\n    .map(predicate => filter.serializePredicate(predicate))\n    .map(encodeURIComponent)\n    .join('+AND+')\n    .value();\n\n  const url = `https://api.atlassian.com/ex/jira/${site}/rest/api/2/search?jql=${filterString}`;\n\n  const cacheKey = `jira.filter.${hash(url)}`;\n\n  const { issues } = await Cache.remember(cacheKey, 60, () =>\n    fetch(url, {\n      method: 'GET',\n      headers: {\n        'Content-Type': 'application/json',\n        Authorization: `Bearer ${token}`,\n      },\n    }).then(res => res.json())\n  );\n\n  return issues;\n}\n"
  },
  {
    "path": "src/providers/jira/fetchers/refreshToken.ts",
    "content": "interface IRefreshResult {\n  access_token: string;\n  expires_in: number;\n}\n\nexport const refreshToken = async (\n  refreshToken: string\n): Promise<IRefreshResult> => {\n  const url = 'https://octolenses.now.sh/api/refresh';\n\n  const { data } = await fetch(url, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({\n      refresh_token: refreshToken,\n    }),\n  }).then(res => res.json());\n\n  return data;\n};\n"
  },
  {
    "path": "src/providers/jira/fetchers/resources.ts",
    "content": "export const fetchResources = async (token: string) => {\n  const url = 'https://api.atlassian.com/oauth/token/accessible-resources';\n\n  const response = await fetch(url, {\n    method: 'GET',\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${token}`,\n    },\n  });\n\n  return await response.json();\n};\n"
  },
  {
    "path": "src/providers/jira/fetchers/swapToken.ts",
    "content": "export interface ISwapResult {\n  access_token: string;\n  refresh_token: string;\n  expires_in: number;\n}\n\nexport const swapToken = async (\n  authCode: string,\n  redirectUri: string\n): Promise<ISwapResult> => {\n  const url = 'https://octolenses.now.sh/api/swap';\n\n  const { data } = await fetch(url, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({\n      code: authCode,\n      redirect_uri: redirectUri,\n    }),\n  }).then(res => res.json());\n\n  return data;\n};\n"
  },
  {
    "path": "src/providers/jira/index.tsx",
    "content": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport { find, get } from 'lodash';\nimport { action, computed, observable } from 'mobx';\nimport hash from 'object-hash';\nimport React from 'react';\n\nimport { Cache } from '../../lib/cache';\nimport { Filter } from '../../store/filters';\nimport { AbstractProvider } from '../AbstractProvider';\nimport { IProps as IIssueCardProps, IssueCard } from './components/IssueCard';\nimport { Settings } from './components/Settings';\nimport { fetchFilter } from './fetchers';\nimport { refreshToken } from './fetchers/refreshToken';\nimport { fetchResources } from './fetchers/resources';\nimport { ISwapResult } from './fetchers/swapToken';\nimport { availablePredicates } from './predicates';\n\nconst FIVE_MINUTES = 5 * 60 * 1000; // ms\n\nexport interface IJiraSettings {\n  auth: {\n    access_token: string;\n    refresh_token: string;\n    expires_at: number;\n  };\n}\n\nexport interface IJiraResource {\n  avatarUrl: string;\n  id: string;\n  name: string;\n  scopes: string[];\n  url: string;\n}\n\nexport class JiraProvider extends AbstractProvider<IJiraSettings> {\n  public id = 'jira';\n  public label = 'Jira';\n  public settingsComponent = () => <Settings provider={this} />;\n  public cardComponent = (props: Omit<IIssueCardProps, 'provider'>) => (\n    <IssueCard {...props} provider={this} />\n  );\n\n  @observable\n  public resources: IJiraResource[] = [];\n\n  public async initialize() {\n    if (this.shouldRefreshToken) {\n      await this.refreshToken();\n    }\n\n    await this.fetchResources();\n  }\n\n  public async fetchFilter(filter: Filter) {\n    return fetchFilter(filter, this.settings, this.resources[0]);\n  }\n\n  public resolveFilterItemIdentifier(item: any): string {\n    return item.key;\n  }\n\n  public getAvailablePredicates = () => availablePredicates;\n\n  public findPredicate(name: string) {\n    return find(this.getAvailablePredicates(), { name });\n  }\n\n  @computed\n  private get shouldRefreshToken() {\n    const refresh_token = get(this.settings, 'auth.refresh_token');\n    const expires_at = get(this.settings, 'auth.expires_at');\n    return refresh_token && expires_at - FIVE_MINUTES <= Date.now();\n  }\n\n  @action.bound\n  public async refreshToken() {\n    const { refresh_token } = this.settings.auth;\n    const { access_token, expires_in } = await refreshToken(refresh_token);\n    this.setAuth({ access_token, expires_in, refresh_token });\n  }\n\n  @action.bound\n  public setAuth({ access_token, expires_in, refresh_token }: ISwapResult) {\n    this.settings.auth = {\n      refresh_token,\n      access_token,\n      expires_at: Date.now() + expires_in * 1000,\n    };\n\n    this.fetchResources();\n  }\n\n  @action.bound\n  public disconnect() {\n    this.settings.auth = null;\n    this.resources = [];\n  }\n\n  @action.bound\n  public async fetchResources() {\n    const token = get(this.settings, 'auth.access_token');\n\n    if (!token) {\n      return;\n    }\n\n    const cacheKey = `jira.resources.${hash(token)}`;\n    this.resources = await Cache.remember(cacheKey, 5 * 60, () =>\n      fetchResources(token)\n    );\n  }\n}\n\nexport const jira = new JiraProvider();\n"
  },
  {
    "path": "src/providers/jira/predicates/index.ts",
    "content": "import { capitalize, chain } from 'lodash';\n\nimport { Predicate, PredicateType } from '../../types';\n\nenum JiraOperators {\n  EQ = '=',\n  NEQ = '!=',\n  GT = '>',\n  GTE = '>=',\n  LT = '<',\n  LTE = '<=',\n  IN = 'IN',\n  NOT_IN = 'NOT IN',\n  CONTAINS = '~',\n  NOT_CONTAINS = '!~',\n}\n\ninterface ISimplePredicatePayload {\n  name: string;\n  placeholder?: string;\n  label?: string;\n  categorical?: boolean;\n  numerical?: boolean;\n  textual?: boolean;\n}\n\n/**\n * Makes a simple text predicate\n * @param options Options configuring the predicate\n */\nexport const makePredicate = ({\n  name,\n  label,\n  placeholder = '',\n  categorical = true,\n  numerical = false,\n  textual = false,\n}: ISimplePredicatePayload): Predicate => ({\n  name,\n  label: label || capitalize(name),\n  placeholder,\n  type: PredicateType.TEXT,\n  operators: makeAvailableOperators(categorical, numerical, textual),\n  serialize: ({ value, operator }) => `${name} ${operator} ${value}`,\n});\n\nconst makeAvailableOperators = (\n  categorical: boolean,\n  numerical: boolean,\n  textual: boolean\n) =>\n  chain([])\n    .concat(\n      numerical && [\n        { value: JiraOperators.EQ, label: '=' },\n        { value: JiraOperators.NEQ, label: '!=' },\n        { value: JiraOperators.GT, label: '>' },\n        { value: JiraOperators.GTE, label: '>=' },\n        { value: JiraOperators.GTE, label: '<' },\n        { value: JiraOperators.GTE, label: '<=' },\n      ],\n      categorical && [\n        { value: JiraOperators.EQ, label: '=' },\n        { value: JiraOperators.NEQ, label: '!=' },\n        { value: JiraOperators.IN, label: 'IN' },\n        { value: JiraOperators.NOT_IN, label: 'NOT IN' },\n      ],\n      textual && [\n        { value: JiraOperators.CONTAINS, label: '~' },\n        { value: JiraOperators.NOT_CONTAINS, label: '!~' },\n      ]\n    )\n    .compact()\n    .uniq()\n    .value();\n\nexport const availablePredicates: Predicate[] = [\n  makePredicate({ name: 'project', placeholder: 'MYPROJECT' }),\n  makePredicate({ name: 'status', placeholder: 'open/closed' }),\n  makePredicate({ name: 'resolution' }),\n  makePredicate({ name: 'sprint' }),\n  makePredicate({ name: 'assignee' }),\n  makePredicate({ name: 'component' }),\n  makePredicate({ name: 'created', numerical: true }),\n  makePredicate({ name: 'creator', numerical: true }),\n  makePredicate({ name: 'label' }),\n  makePredicate({ name: 'level' }),\n  makePredicate({ name: 'priority', numerical: true }),\n  makePredicate({ name: 'reporter' }),\n  makePredicate({ name: 'resolved', numerical: true }),\n  makePredicate({ name: 'timeSpent', numerical: true }),\n  makePredicate({ name: 'type' }),\n  makePredicate({ name: 'updated', numerical: true }),\n  makePredicate({ name: 'workRatio', label: 'Work Ratio', numerical: true }),\n  makePredicate({ name: 'comment', categorical: false, textual: true }),\n  makePredicate({ name: 'description', categorical: false, textual: true }),\n  makePredicate({ name: 'summary', categorical: false, textual: true }),\n  makePredicate({ name: 'text', categorical: false, textual: true }),\n  makePredicate({\n    name: 'issueKey',\n    label: 'Issue Key',\n    numerical: true,\n  }),\n  makePredicate({\n    name: 'remainingEstimate',\n    label: 'Remaining Estimate',\n    numerical: true,\n  }),\n];\n"
  },
  {
    "path": "src/providers/types.ts",
    "content": "export enum ProviderType {\n  GITHUB = 'github',\n  JIRA = 'jira',\n}\n\ntype PredicateIdentifier = string;\n\n// Template predicate, as returned by providers\ninterface IBasePredicate {\n  type: PredicateType;\n  name: PredicateIdentifier;\n  label: string;\n  operators: IPredicateOperator[];\n  serialize?: (payload: { value: string; operator?: string }) => string;\n}\n\n// A simple text predicate\ninterface ITextPredicate extends IBasePredicate {\n  type: PredicateType.TEXT;\n  placeholder: string;\n}\n\n// A dropdown predicate, allowing to pick from multiple choices\nexport interface IDropdownPredicate extends IBasePredicate {\n  type: PredicateType.DROPDOWN;\n  choices: Array<{ value: string; label: string }>;\n}\n\nexport type Predicate = ITextPredicate | IDropdownPredicate;\n\n// A predicate once it's been stored and configured\nexport interface IStoredPredicate {\n  type: PredicateIdentifier;\n  value: string;\n  operator?: string;\n}\n\nexport enum PredicateType {\n  TEXT = 'text',\n  DROPDOWN = 'dropdown',\n}\n\ninterface IPredicateOperator {\n  value: string;\n  label: string;\n}\n"
  },
  {
    "path": "src/service_worker/cache.js",
    "content": "import 'babel-polyfill';\nimport { difference } from 'lodash';\n\nconst PRECACHE = 'precache-v1';\nconst RUNTIME = 'runtime';\n\n// List of URLs of resources we want to always serve from the cache.\nconst PRECACHE_URLS = [\n  'https://use.fontawesome.com/releases/v5.6.0/css/all.css',\n  'https://use.fontawesome.com/releases/v5.6.0/webfonts/fa-brands-400.woff2',\n  'https://use.fontawesome.com/releases/v5.6.0/webfonts/fa-solid-900.woff2',\n  'https://use.fontawesome.com/releases/v5.6.0/webfonts/fa-regular-400.woff2',\n  'https://fonts.googleapis.com/css?family=Open+Sans|Roboto:400,500',\n];\n\n// On install of the Service Worker, precache all the resources that need to.\nself.addEventListener('install', event => {\n  event.waitUntil(\n    caches\n      .open(PRECACHE)\n      .then(cache => cache.addAll(PRECACHE_URLS))\n      .then(self.skipWaiting())\n  );\n});\n\n// On activation of the Service Worker, remove the old cache buckets.\nself.addEventListener('activate', event => {\n  const currentCaches = [PRECACHE, RUNTIME];\n\n  event.waitUntil(\n    caches\n      .keys()\n      .then(cacheNames => difference(cacheNames, currentCaches))\n      .then(cachesToDelete => {\n        return Promise.all(\n          cachesToDelete.map(cacheToDelete => {\n            return caches.delete(cacheToDelete);\n          })\n        );\n      })\n      .then(() => self.clients.claim())\n  );\n});\n"
  },
  {
    "path": "src/service_worker/index.js",
    "content": "import './cache.js';\n\nfunction openInNewTab() {\n  chrome.tabs.create({\n    url: chrome.runtime.getURL('index.html'),\n  });\n}\n\n// On click of the extension's icon, open OctoLenses in a new tab.\nchrome.action.onClicked.addListener(openInNewTab);\n"
  },
  {
    "path": "src/setupTests.ts",
    "content": "require('jest-localstorage-mock'); // tslint:disable-line no-var-requires\n"
  },
  {
    "path": "src/store/filters.ts",
    "content": "import { find, findIndex } from 'lodash';\nimport { action, computed, observable } from 'mobx';\nimport { persist } from 'mobx-persist';\nimport { arrayMove } from 'react-sortable-hoc';\n\nimport { ProviderType } from '../providers';\nimport { Filter, FilterIdentifier } from './models/filter';\nimport { settingsStore } from './settings';\n\nexport { Filter };\n\nexport class FiltersStore {\n  @persist('list', Filter)\n  @observable\n  private data: Filter[] = [];\n\n  @computed\n  get count() {\n    return this.data.length;\n  }\n\n  public findFilter(id: string) {\n    return find(this.data, { id });\n  }\n\n  public findFilterIndex(id: string) {\n    return findIndex(this.data, { id });\n  }\n\n  public getFilters() {\n    return this.data;\n  }\n\n  public getFilterAt(index: number) {\n    return this.data[index];\n  }\n\n  public getFirstFilter() {\n    return this.data[0] || null;\n  }\n\n  // TODO Any\n  @action.bound\n  public saveFilter(filterPayload: any) {\n    const index = findIndex(this.data, { id: filterPayload.id });\n\n    // If we're saving a filter that already exists, we only need to update\n    // some of its attributes\n    if (index !== -1) {\n      this.data[index].update(filterPayload);\n      return;\n    }\n\n    // Else create a new filter\n    const filter = Filter.fromAttributes(filterPayload);\n    this.data.push(filter);\n    filter.fetchFilter();\n    settingsStore.selectedFilterId = filter.id;\n  }\n\n  @action.bound\n  public cloneFilter(id: FilterIdentifier) {\n    const index = findIndex(this.data, { id });\n    const filter = this.data[index];\n\n    const clonedFilter = filter.clone();\n\n    this.data.splice(index + 1, 0, clonedFilter);\n\n    return clonedFilter;\n  }\n\n  @action.bound\n  public removeFilter(id: FilterIdentifier) {\n    const index = findIndex(this.data, { id });\n    this.data.splice(index, 1);\n  }\n\n  @action.bound\n  public swapFilters(oldIndex: number, newIndex: number) {\n    this.data = arrayMove(this.data, oldIndex, newIndex);\n  }\n\n  public async fetchAllFilters() {\n    await Promise.all(this.data.map(filter => filter.invalidateCache()));\n  }\n}\n\nexport const EMPTY_FILTER_PAYLOAD = {\n  provider: ProviderType.GITHUB,\n  label: 'Unnamed filter',\n  predicates: [{ type: 'status', value: 'open' }],\n};\n\nexport const filtersStore = new FiltersStore();\n"
  },
  {
    "path": "src/store/index.ts",
    "content": "import { chain } from 'lodash';\nimport { create } from 'mobx-persist';\n\nimport { Cache } from '../lib/cache';\nimport { migrator } from '../migrations';\nimport { providers, ProviderType } from '../providers';\n\nimport { filtersStore } from './filters';\nimport { navigationStore } from './navigation';\nimport { settingsStore } from './settings';\nimport { trendsStore } from './trends';\n\nconst hydrateStores = async () => {\n  const hydrate = create({});\n\n  // Re-hydrate the stores\n  await Promise.all([\n    hydrate('navigationStore', navigationStore),\n    hydrate('settingsStore', settingsStore),\n    hydrate('filtersStore', filtersStore),\n  ]);\n\n  // Re-hydrate the providers\n  await Promise.all(\n    chain(providers)\n      .values()\n      .map(provider => hydrate(`${provider.id}Provider`, provider))\n      .value()\n  );\n};\n\nconst initializeProviders = async () => {\n  await Promise.all(\n    chain(providers)\n      .values()\n      .map(provider => provider.initialize())\n      .value()\n  );\n};\n\nconst performOnboarding = () => {\n  if (settingsStore.wasOnboarded) {\n    return;\n  }\n\n  filtersStore.saveFilter({\n    label: 'OctoLenses Issues',\n    provider: ProviderType.GITHUB,\n    data: [],\n    loading: false,\n    predicates: [\n      { type: 'type', value: 'issue' },\n      { type: 'repo', value: 'rgehan/octolenses' },\n      { type: 'status', value: 'open' },\n    ],\n  });\n\n  settingsStore.updateWasOnboarded(true);\n};\n\nexport const refreshAllData = async () => {\n  // prettier-ignore\n  await Promise.all([\n    trendsStore.fetchTrendingRepos(),\n    filtersStore.fetchAllFilters(),\n  ]);\n};\n\nexport const bootstrap = async () => {\n  migrator.migrate();\n  await hydrateStores();\n  await initializeProviders();\n  performOnboarding();\n  await refreshAllData();\n  Cache.flushExpired();\n};\n\n// This shouldn't be typed, as we don't want to advertize that this is available\n// on the global window object. It's only there for debugging purposes\n(window as any).stores = {\n  navigationStore,\n  filtersStore,\n  trendsStore,\n  settingsStore,\n};\n\nexport { navigationStore, filtersStore, trendsStore, settingsStore };\n"
  },
  {
    "path": "src/store/models/filter.ts",
    "content": "import { difference, map, merge } from 'lodash';\nimport { action, computed, observable, reaction } from 'mobx';\nimport { persist } from 'mobx-persist';\nimport hash from 'object-hash';\nimport uuidv1 from 'uuid/v1';\n\nimport { toast } from '../../components/ToastManager';\nimport { providers, ProviderType, IStoredPredicate } from '../../providers';\n\nexport type FilterIdentifier = string;\n\nexport class Filter {\n  @persist\n  public provider: ProviderType;\n\n  @persist\n  @observable\n  public id: FilterIdentifier;\n\n  @persist\n  @observable\n  public label = '';\n\n  @persist('list')\n  @observable\n  public predicates: IStoredPredicate[] = [];\n\n  @observable\n  public data: any[] = []; // TODO\n\n  @observable\n  public loading = true;\n\n  @observable\n  public error: Error = null;\n\n  @persist\n  @observable\n  public lastModified = 0;\n\n  @persist('list')\n  @observable\n  private previousItemsIdentifiers: string[] = [];\n\n  @observable\n  private newItemsIdentifiers: string[] = [];\n\n  @observable\n  private disableNotificationsOnNextFetch: false;\n\n  constructor() {\n    // When the hash of the filter changes, re-fetch it\n    reaction(\n      () => this.hash,\n      () => {\n        this.fetchFilter();\n      }\n    );\n  }\n\n  /*\n   * Static\n   */\n\n  public static fromAttributes({ id, ...otherAttributes }: any) {\n    const filter = new Filter();\n    filter.id = id || uuidv1();\n    merge(filter, otherAttributes);\n    return filter;\n  }\n\n  /*\n   * Public API\n   */\n\n  public serializePredicate(payload: IStoredPredicate): string {\n    const provider = providers[this.provider];\n    const predicate = provider.findPredicate(payload.type);\n    return predicate.serialize(payload);\n  }\n\n  public clone(): Filter {\n    return Filter.fromAttributes({\n      provider: this.provider,\n      label: `${this.label} (Copy)`,\n      predicates: this.predicates,\n      data: this.data,\n      loading: this.loading,\n      error: this.error,\n      lastModified: this.lastModified,\n      previousItemsIdentifiers: this.previousItemsIdentifiers,\n      newItemsIdentifiers: this.newItemsIdentifiers,\n    });\n  }\n\n  public isItemNew(item: any) {\n    const identifier = this.getItemIdentifier(item);\n    return this.newItemsIdentifiers.includes(identifier);\n  }\n\n  public clearNewItemsNotifications() {\n    this.newItemsIdentifiers = [];\n  }\n\n  public update(payload: any) {\n    Object.assign(this, {\n      disableNotificationsOnNextFetch: true,\n      ...payload,\n    });\n  }\n\n  /*\n   * Computed\n   */\n\n  @computed\n  public get hash(): string {\n    return hash({\n      id: this.id,\n      lastModified: this.lastModified,\n      predicates: this.predicates,\n    });\n  }\n\n  @computed\n  public get newItemsCount() {\n    return this.newItemsIdentifiers.length;\n  }\n\n  /*\n   * Actions\n   */\n\n  @action.bound\n  public async fetchFilter() {\n    this.loading = true;\n    this.error = null;\n\n    try {\n      const result = await providers[this.provider].fetchFilter(this);\n      this.setData(result);\n    } catch (error) {\n      toast('Oops, something failed with your filter!', 'error');\n      this.error = error;\n    }\n\n    this.loading = false;\n  }\n\n  @action.bound\n  public invalidateCache() {\n    this.lastModified = Date.now();\n  }\n\n  @action.bound\n  public setData(data: any) {\n    const currentItemsIdentifiers = this.getItemsIdentifiers(data);\n\n    // Update the data\n    this.data = data;\n\n    if (!this.disableNotificationsOnNextFetch) {\n      // Store the IDs of the items that weren't previously known\n      this.newItemsIdentifiers = difference(\n        currentItemsIdentifiers,\n        this.previousItemsIdentifiers\n      );\n    } else {\n      this.newItemsIdentifiers = [];\n      this.disableNotificationsOnNextFetch = false;\n    }\n\n    // Store the IDs of the current items, for later comparison\n    this.previousItemsIdentifiers = currentItemsIdentifiers;\n  }\n\n  private getItemsIdentifiers(items: any[]) {\n    return map(items, this.getItemIdentifier);\n  }\n\n  private getItemIdentifier = (item: any) => {\n    const provider = providers[this.provider];\n    return provider.resolveFilterItemIdentifier(item);\n  };\n}\n"
  },
  {
    "path": "src/store/navigation.ts",
    "content": "import { action, observable } from 'mobx';\nimport { persist } from 'mobx-persist';\n\nexport class NavigationStore {\n  @persist\n  @observable\n  public page = 'dashboard';\n\n  @action.bound\n  public navigateTo(newPage: string) {\n    this.page = newPage;\n  }\n}\n\nexport const navigationStore = new NavigationStore();\n"
  },
  {
    "path": "src/store/settings.ts",
    "content": "import { action, autorun, observable } from 'mobx';\nimport { persist } from 'mobx-persist';\n\nimport { DARK_MODE } from '../constants/darkMode';\nimport { DATES, DateType } from '../constants/dates';\nimport { LANGUAGES } from '../constants/languages';\n\nexport class SettingsStore {\n  /**\n   * Language used in the \"Discover\" page\n   */\n  @persist\n  @observable\n  public language = LANGUAGES[0].value;\n\n  /**\n   * How far in the past to find repos in the \"Discover\" page\n   */\n  @persist\n  @observable\n  public dateRange = DATES[0].value;\n\n  /**\n   * DEPRECATED. Legacy place where the GitHub token was stored.\n   */\n  @persist\n  @observable\n  public token: string = undefined;\n\n  /**\n   * Current dark mode state. Whether it's always on/off, or only at night.\n   */\n  @persist\n  @observable\n  public darkMode = DARK_MODE.DISABLED;\n\n  /**\n   * Whether the \"onboarding\" was run\n   */\n  @persist\n  @observable\n  public wasOnboarded = false;\n\n  /**\n   * Id of the filter that is selected in the sidebar\n   */\n  @persist\n  @observable\n  public selectedFilterId: string = null;\n\n  /**\n   * Version of the current settings schema. It is used to determine which\n   * migrations to run.\n   */\n  @persist\n  @observable\n  public schemaVersion = 3;\n\n  @observable\n  public isDark = false;\n\n  public applyDarkMode() {\n    const hours = new Date().getHours();\n    const isNightTime = hours >= 19 || hours <= 7;\n\n    if (this.darkMode === DARK_MODE.ENABLED) {\n      this.isDark = true;\n    } else if (this.darkMode === DARK_MODE.AT_NIGHT && isNightTime) {\n      this.isDark = true;\n    } else {\n      this.isDark = false;\n    }\n\n    document.body.className = this.isDark ? 'dark' : 'light';\n  }\n\n  @action.bound\n  public updateDarkMode(darkMode: string) {\n    this.darkMode = darkMode;\n  }\n\n  @action.bound\n  public updateWasOnboarded(wasOnboarded: boolean) {\n    this.wasOnboarded = wasOnboarded;\n  }\n\n  @action.bound\n  public updateLanguage(language: string) {\n    this.language = language;\n  }\n\n  @action.bound\n  public updateDateRange(dateRange: DateType) {\n    this.dateRange = dateRange;\n  }\n}\n\nexport const settingsStore = new SettingsStore();\n\n// Apply darkMode on the page whenever it changes\nautorun(() => {\n  settingsStore.applyDarkMode();\n});\n\n// Update dark mode periodically in case it's now the night\nsetInterval(() => {\n  settingsStore.applyDarkMode();\n}, 1000);\n"
  },
  {
    "path": "src/store/trends.ts",
    "content": "import { action, observable } from 'mobx';\n\nimport { getDateFromValue } from '../constants/dates';\nimport { fetchTrendingRepos } from '../lib/github';\nimport { settingsStore } from './settings';\n\nexport class TrendsStore {\n  @observable\n  public data: any[] = [];\n\n  @observable\n  public loading = false;\n\n  @action.bound\n  public updateRepos(newRepos: any[]) {\n    this.data = newRepos;\n  }\n\n  @action.bound\n  public setLoading(isLoading: boolean) {\n    this.loading = isLoading;\n  }\n\n  @action.bound\n  public async fetchTrendingRepos() {\n    const language = settingsStore.language;\n    const date = getDateFromValue(settingsStore.dateRange);\n    const token = settingsStore.token;\n\n    this.loading = true;\n    this.data = await fetchTrendingRepos({ language, date, token });\n    this.loading = false;\n  }\n}\n\nexport const trendsStore = new TrendsStore();\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "module.exports = {\n  theme: {\n    fontFamily: {\n      open: ['Open Sans', 'sans-serif'],\n      roboto: ['Roboto', 'sans-serif'],\n      mono: [\n        'Menlo',\n        'Monaco',\n        'Consolas',\n        'Liberation Mono',\n        'Courier New',\n        'monospace',\n      ],\n    },\n    extend: {\n      fontSize: {\n        '2xs': '.625rem', // 10px\n      },\n      inset: {\n        '4': '1rem',\n      },\n    },\n    screens: {\n      sm: '640px',\n      md: '768px',\n      lg: '1024px',\n      xl: '1280px',\n    },\n    fontSize: {\n      xs: '0.75rem',\n      sm: '0.875rem',\n      base: '1rem',\n      lg: '1.125rem',\n      xl: '1.25rem',\n      '2xl': '1.5rem',\n      '3xl': '1.875rem',\n      '4xl': '2.25rem',\n      '5xl': '3rem',\n      '6xl': '4rem',\n    },\n    colors: {\n      transparent: 'transparent',\n      current: 'currentColor',\n\n      black: '#000',\n      white: '#fff',\n\n      gray: {\n        100: '#f7fafc',\n        200: '#edf2f7',\n        300: '#e2e8f0',\n        400: '#cbd5e0',\n        500: '#a0aec0',\n        600: '#718096',\n        700: '#4a5568',\n        800: '#2d3748',\n        900: '#1a202c',\n      },\n      red: {\n        100: '#fff5f5',\n        200: '#fed7d7',\n        300: '#feb2b2',\n        400: '#fc8181',\n        500: '#f56565',\n        600: '#e53e3e',\n        700: '#c53030',\n        800: '#9b2c2c',\n        900: '#742a2a',\n      },\n      orange: {\n        100: '#fffaf0',\n        200: '#feebc8',\n        300: '#fbd38d',\n        400: '#f6ad55',\n        500: '#ed8936',\n        600: '#dd6b20',\n        700: '#c05621',\n        800: '#9c4221',\n        900: '#7b341e',\n      },\n      yellow: {\n        100: '#fffff0',\n        200: '#fefcbf',\n        300: '#faf089',\n        400: '#f6e05e',\n        500: '#ecc94b',\n        600: '#d69e2e',\n        700: '#b7791f',\n        800: '#975a16',\n        900: '#744210',\n      },\n      green: {\n        100: '#f0fff4',\n        200: '#c6f6d5',\n        300: '#9ae6b4',\n        400: '#68d391',\n        500: '#48bb78',\n        600: '#38a169',\n        700: '#2f855a',\n        800: '#276749',\n        900: '#22543d',\n      },\n      teal: {\n        100: '#e6fffa',\n        200: '#b2f5ea',\n        300: '#81e6d9',\n        400: '#4fd1c5',\n        500: '#38b2ac',\n        600: '#319795',\n        700: '#2c7a7b',\n        800: '#285e61',\n        900: '#234e52',\n      },\n      blue: {\n        100: '#ebf8ff',\n        200: '#bee3f8',\n        300: '#90cdf4',\n        400: '#63b3ed',\n        500: '#4299e1',\n        600: '#3182ce',\n        700: '#2b6cb0',\n        800: '#2c5282',\n        900: '#2a4365',\n      },\n      indigo: {\n        100: '#ebf4ff',\n        200: '#c3dafe',\n        300: '#a3bffa',\n        400: '#7f9cf5',\n        500: '#667eea',\n        600: '#5a67d8',\n        700: '#4c51bf',\n        800: '#434190',\n        900: '#3c366b',\n      },\n      purple: {\n        100: '#faf5ff',\n        200: '#e9d8fd',\n        300: '#d6bcfa',\n        400: '#b794f4',\n        500: '#9f7aea',\n        600: '#805ad5',\n        700: '#6b46c1',\n        800: '#553c9a',\n        900: '#44337a',\n      },\n      pink: {\n        100: '#fff5f7',\n        200: '#fed7e2',\n        300: '#fbb6ce',\n        400: '#f687b3',\n        500: '#ed64a6',\n        600: '#d53f8c',\n        700: '#b83280',\n        800: '#97266d',\n        900: '#702459',\n      },\n    },\n  },\n  variants: {\n    backgroundColor: ['responsive', 'hover', 'active'],\n  },\n  plugins: [],\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"outDir\": \"./dist/\",\n    \"allowJs\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"lib\": [\"dom\", \"es2015\", \"es2016\", \"es2017\", \"es2018\"],\n    \"jsx\": \"react\",\n    \"target\": \"es6\",\n    \"module\": \"esNext\",\n    \"moduleResolution\": \"node\",\n    \"noImplicitAny\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"removeComments\": false,\n    \"preserveConstEnums\": true,\n    \"sourceMap\": true,\n    \"skipLibCheck\": true,\n    \"noEmitOnError\": false,\n    \"experimentalDecorators\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"chrome\", \"jest\", \"node\"]\n  },\n  \"include\": [\"./src/**/*.ts\", \"./src/**/*.tsx\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  }
]