[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [Akryum]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\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 - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, 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\ntitle: ''\nlabels: ''\nassignees: ''\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/lint.yml",
    "content": "name: Lint\n\non: [push, pull_request]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n    \n    steps:\n    - uses: actions/checkout@v1\n    - name: Install deps\n      run: yarn install --pure-lockfile\n    - name: Linting files...\n      run: npx lerna run lint -- --no-fix\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\n.DS_Store\n*.log\npackages/test\ndist\n/.env\n/*.sh\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at guillaume@moonreach.io. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Guillaume Chau\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": "# awesomejs.dev\nFind awesome packages for the framework you are using\n\n## Sponsors\n\n[![sponsors logos](https://guillaume-chau.info/sponsors.png)](https://guillaume-chau.info/sponsors)\n"
  },
  {
    "path": "lerna.json",
    "content": "{\n  \"npmClient\": \"yarn\",\n  \"useWorkspaces\": true,\n  \"version\": \"0.1.0\",\n  \"packages\": [\n    \"packages/*\"\n  ]\n}\n"
  },
  {
    "path": "now.json",
    "content": "{\n  \"version\": 2,\n  \"regions\": [\n    \"all\"\n  ],\n  \"builds\": [\n    {\n      \"src\": \"packages/frontend/package.json\",\n      \"use\": \"@now/static-build\",\n      \"config\": {\n        \"distDir\": \"dist\"\n      }\n    },\n    {\n      \"src\": \"packages/backend/dist/app.js\",\n      \"use\": \"@now/node\",\n      \"config\": {\n        \"includeFiles\": [\n          \"packages/backend/dist/**\"\n        ]\n      }\n    }\n  ],\n  \"routes\": [\n    {\n      \"src\": \"/\",\n      \"dest\": \"/packages/frontend/index.html\"\n    },\n    {\n      \"src\": \"/api\",\n      \"dest\": \"/packages/backend/dist/app.js\"\n    },\n    {\n      \"src\": \"/api/(.*)\",\n      \"dest\": \"/packages/backend/dist/app.js\"\n    },\n    {\n      \"src\": \"/.well-known/apollo/server-health\",\n      \"headers\": { \"cache-control\": \"s-maxage=0\" },\n      \"dest\": \"/packages/backend/dist/app.js\"\n    },\n    {\n      \"src\": \"^/service-worker.js\",\n      \"headers\": { \"cache-control\": \"s-maxage=0\" },\n      \"dest\": \"/packages/frontend/service-worker.js\"\n    },\n    {\n      \"src\": \"/(.*\\\\.(ico|js|json|css|svg|png|jpg|txt))\",\n      \"headers\": { \"cache-control\": \"s-maxage=31536000\" },\n      \"dest\": \"/packages/frontend/$1\"\n    },\n    {\n      \"src\": \"/(.*)\",\n      \"dest\": \"/packages/frontend/index.html\"\n    }\n  ],\n  \"env\": {\n    \"PORT\": \"4040\",\n    \"BASE_API_PATH\": \"/api\",\n    \"CLIENT_BASE_URL\": \"\",\n    \"DB_SECRET\": \"@awesomejs_db_secret\",\n    \"GITHUB_AUTH\": \"@awesomejs_github_auth\",\n    \"COOKIE_SECRET\": \"@awesomejs_cookie_secret\",\n    \"ALGOLIA_ID\": \"@awesomejs_algolia_id\",\n    \"ALGOLIA_KEY\": \"@awesomejs_algolia_key\",\n    \"APOLLO_ENGINE_KEY\": \"@awesomejs_apollo_engine_key\",\n    \"APOLLO_ENGINE_SCHEMA_TAG\": \"prod\",\n    \"OAUTH_GITHUB_ID\": \"@awesomejs_oauth_github_id\",\n    \"OAUTH_GITHUB_SECRET\": \"@awesomejs_oauth_github_secret\",\n    \"OAUTH_GITHUB_CALLBACK\": \"https://awesomejs.dev/api/auth/github/callback\"\n  },\n  \"build\": {\n    \"env\": {\n      \"DB_SECRET\": \"@awesomejs_db_secret\",\n      \"GITHUB_AUTH\": \"@awesomejs_github_auth\",\n      \"ALGOLIA_ID\": \"@awesomejs_algolia_id\",\n      \"ALGOLIA_KEY\": \"@awesomejs_algolia_key\",\n      \"VUE_APP_ALGOLIA_ID\": \"@awesomejs_algolia_id\",\n      \"VUE_APP_ALGOLIA_KEY\": \"@awesomejs_algolia_key\",\n      \"VUE_APP_GRAPHQL_HTTP\": \"/api/graphql\",\n      \"VUE_APP_API_BASE\": \"/api\"\n    }\n  },\n  \"github\": {\n    \"autoAlias\": false\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"awesomejs.dev\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Find awesome packages for the framework you are using\",\n  \"private\": true,\n  \"workspaces\": [\n    \"packages/*\"\n  ],\n  \"scripts\": {\n    \"build\": \"lerna run build\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/Akryum/awesomejs.dev.git\"\n  },\n  \"author\": \"Guillaume Chau <guillaume.b.chau@gmail.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/Akryum/awesomejs.dev/issues\"\n  },\n  \"homepage\": \"https://github.com/Akryum/awesomejs.dev#readme\",\n  \"devDependencies\": {\n    \"lerna\": \"^3.16.4\"\n  }\n}\n"
  },
  {
    "path": "packages/backend/.gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw*\n"
  },
  {
    "path": "packages/backend/.nodepack/.gitignore",
    "content": "/temp\n/env-migration-records.json\n/config.json\n"
  },
  {
    "path": "packages/backend/.nodepack/README.md",
    "content": "# Nodepack internal config files\n\nAdd this folder to version control. Modify at yourn own risk!\n"
  },
  {
    "path": "packages/backend/.nodepack/app-migration-plugin-versions.json",
    "content": "{\n  \"@nodepack/service\": \"0.7.0\",\n  \"@nodepack/plugin-apollo\": \"0.7.0\",\n  \"@nodepack/plugin-express\": \"0.7.0\",\n  \"@nodepack/plugin-typescript\": \"0.7.0\",\n  \"@nodepack/plugin-babel\": \"0.7.0\",\n  \"@nodepack/plugin-passport\": \"0.7.0\"\n}\n"
  },
  {
    "path": "packages/backend/.nodepack/app-migration-records.json",
    "content": "[\n  {\n    \"id\": \"defaultTemplate\",\n    \"pluginId\": \"@nodepack/service\",\n    \"pluginVersion\": \"0.4.11\",\n    \"options\": {},\n    \"date\": \"2019-09-30T17:11:45.061Z\"\n  },\n  {\n    \"id\": \"defaultTemplate\",\n    \"pluginId\": \"@nodepack/plugin-typescript\",\n    \"pluginVersion\": \"0.4.5\",\n    \"options\": {\n      \"tslint\": true\n    },\n    \"date\": \"2019-09-30T17:11:45.074Z\"\n  },\n  {\n    \"id\": \"configPath\",\n    \"pluginId\": \"@nodepack/plugin-typescript\",\n    \"pluginVersion\": \"0.4.5\",\n    \"options\": {},\n    \"date\": \"2019-09-30T17:11:45.300Z\"\n  },\n  {\n    \"id\": \"defaultRoutes\",\n    \"pluginId\": \"@nodepack/plugin-express\",\n    \"pluginVersion\": \"0.4.7\",\n    \"options\": {},\n    \"date\": \"2019-09-30T17:11:45.326Z\"\n  },\n  {\n    \"id\": \"defaultPackage\",\n    \"pluginId\": \"@nodepack/plugin-apollo\",\n    \"pluginVersion\": \"0.4.7\",\n    \"options\": {},\n    \"date\": \"2019-09-30T17:11:45.334Z\"\n  },\n  {\n    \"id\": \"defaultSchema\",\n    \"pluginId\": \"@nodepack/plugin-apollo\",\n    \"pluginVersion\": \"0.4.7\",\n    \"options\": {},\n    \"date\": \"2019-09-30T17:11:45.343Z\"\n  },\n  {\n    \"id\": \"contextTypePath\",\n    \"pluginId\": \"@nodepack/plugin-typescript\",\n    \"pluginVersion\": \"0.6.0\",\n    \"options\": {},\n    \"date\": \"2019-11-05T13:08:27.307Z\"\n  },\n  {\n    \"id\": \"skipLibCheck\",\n    \"pluginId\": \"@nodepack/plugin-typescript\",\n    \"pluginVersion\": \"0.7.0\",\n    \"options\": {},\n    \"date\": \"2019-11-05T15:53:31.756Z\"\n  }\n]\n"
  },
  {
    "path": "packages/backend/README.md",
    "content": "# awesomejs-backend\n\n## Project setup\n\n```\nyarn install\n```\n\n### Env\n\nCreate a `.env.local` file next to this `README.md` with the necessary secret variables:\n\n```\nDB_SECRET=xxxxxxxxxxxxxxxxxxxxx\nGITHUB_AUTH=xxxxxxxxxxxxxxxxxxxxx\nCOOKIE_SECRET=xxxxxxxxxxxxxxxxxxxxx\nALGOLIA_ID=xxxxxxxxxxxxxxxxxxxxx\nALGOLIA_KEY=xxxxxxxxxxxxxxxxxxxxx\nOAUTH_GITHUB_ID=xxxxxxxxxxxxxxxxxxxxx\nOAUTH_GITHUB_SECRET=xxxxxxxxxxxxxxxxxxxxx\nOAUTH_GITHUB_CALLBACK=http://localhost:4040/auth/github/callback\n```\n\n- `DB_SECRET` must be a valid FaunaDB secret\n- `GITHUB_AUTH` mush be a valid GitHub personal access token\n- `COOKIE_SECRET` must be a random string, ideally pretty long\n- `ALGOLIA_ID` is the Algolia application ID\n- `ALGOLIA_KEY` is the Algolia Admin API key\n- `OAUTH_GITHUB_ID` is the OAuth Client ID\n- `OAUTH_GITHUB_SECRET` is the OAuth Secret\n\n### Customize configuration\nSee [Configuration Reference](https://github.com/Akryum/nodepack).\n"
  },
  {
    "path": "packages/backend/apollo.config.js",
    "content": "const path = require('path')\n\n// Load .env files\nconst { loadEnv } = require('vue-cli-plugin-apollo/utils/load-env')\nconst env = loadEnv([\n  path.resolve(__dirname, '.env'),\n  path.resolve(__dirname, '.env.local')\n])\n\nmodule.exports = {\n  service: {\n    endpoint: {\n      url: 'http://localhost:4040/graphql'\n    }\n  },\n  engine: {\n    apiKey: env.VUE_APP_APOLLO_ENGINE_KEY\n  }\n}\n"
  },
  {
    "path": "packages/backend/config/algolia.ts",
    "content": "export default {\n  id: process.env.ALGOLIA_ID,\n  key: process.env.ALGOLIA_KEY,\n}\n"
  },
  {
    "path": "packages/backend/config/apollo.ts",
    "content": "import { ApolloConfig } from '@nodepack/plugin-apollo'\nimport chalk from 'chalk'\n\nconst basePath = process.env.BASE_API_PATH || ''\n\nexport default {\n  path: `${basePath}/graphql`,\n  subscriptionsPath: `${basePath}/subscriptions`,\n  playground: `${basePath}/playground`,\n  apolloServerOptions: {\n    formatError: (error) => {\n      console.log(chalk.red('Error'), error.stack || error)\n      if (error.extensions.exception) {\n        console.log(chalk.red('Related exception:'), error.extensions.exception)\n      }\n      return error\n    },\n    engine: {\n      apiKey: process.env.APOLLO_ENGINE_KEY,\n      schemaTag: process.env.APOLLO_ENGINE_SCHEMA_TAG,\n    },\n  },\n} as ApolloConfig\n"
  },
  {
    "path": "packages/backend/config/cookie.ts",
    "content": "import { CookieOptions } from 'express'\n\nexport default {\n  secret: process.env.COOKIE_SECRET,\n  domain: process.env.COOKIE_DOMAIN,\n} as CookieOptions\n"
  },
  {
    "path": "packages/backend/config/cors.ts",
    "content": "import { CorsOptions } from 'cors'\n\nexport default {\n  origin: process.env.CLIENT_BASE_URL,\n  methods: ['OPTIONS', 'POST'],\n  credentials: true,\n} as CorsOptions\n"
  },
  {
    "path": "packages/backend/config/db.ts",
    "content": "export default {\n  secret: process.env.DB_SECRET,\n}\n"
  },
  {
    "path": "packages/backend/config/github.ts",
    "content": "import { Options } from '@octokit/rest'\n\nexport default {\n  auth: process.env.GITHUB_AUTH,\n} as Options\n"
  },
  {
    "path": "packages/backend/gql-codegen.yml",
    "content": "schema: http://localhost:4040/graphql\ngenerates:\n  src/generated/schema.ts:\n    plugins:\n      - typescript\n      - typescript-resolvers\n    config:\n      contextType: '@/context#Context'\n      mappers:\n        PackageInterface: '@/schema/package-interface/db-types#DBPackageInterface'\n        PackageProposal: '@/schema/package-proposal/db-types#DBPackageProposal'\n        ProjectType: '@/schema/project-type/db-types#DBProjectType'\n        User: '@/schema/user/db-types#DBUser'\n        UserAccount: '@/schema/user/db-types#DBUserAccount'\n      optionalResolveType: true\n  schema-fragment-matcher.js:\n    - fragment-matcher"
  },
  {
    "path": "packages/backend/nodepack.config.js",
    "content": "/** @type {import('@nodepack/service').ProjectOptions} */\nmodule.exports = {\n  minify: false,\n  externals: true,\n}\n"
  },
  {
    "path": "packages/backend/package.json",
    "content": "{\n  \"name\": \"backend\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"nodepack-service dev\",\n    \"build\": \"nodepack-service build --no-preInstall --no-maintenance\",\n    \"lint\": \"nodepack-service lint\",\n    \"now-build\": \"yarn run build\",\n    \"start\": \"node -r dotenv/config ./dist/app.js dotenv_config_path=.env.local\",\n    \"schema-gen\": \"graphql-codegen --config gql-codegen.yml\"\n  },\n  \"dependencies\": {\n    \"@awesomejs/shared-utils\": \"^1.0.0\",\n    \"@graphql-codegen/typescript-resolvers\": \"^1.9.1\",\n    \"@nodepack/app\": \"^0.8.0\",\n    \"@nodepack/app-context\": \"^0.8.0\",\n    \"@octokit/rest\": \"16.30.1\",\n    \"algoliasearch\": \"^3.35.0\",\n    \"faunadb\": \"^2.8.1\",\n    \"graphql\": \"^14.4.2\",\n    \"graphql-tag\": \"^2.10.1\",\n    \"graphql-type-json\": \"^0.3.1\",\n    \"morgan\": \"^1.9.1\",\n    \"ms\": \"^2.1.2\",\n    \"node-fetch\": \"^2.6.0\",\n    \"npm-registry-fetch\": \"^4.0.1\",\n    \"p-memoize\": \"^3.1.0\",\n    \"passport-github2\": \"^0.1.11\"\n  },\n  \"devDependencies\": {\n    \"@graphql-codegen/cli\": \"^1.9.1\",\n    \"@graphql-codegen/fragment-matcher\": \"^1.9.1\",\n    \"@graphql-codegen/typescript\": \"^1.9.1\",\n    \"@nodepack/plugin-apollo\": \"^0.8.0\",\n    \"@nodepack/plugin-babel\": \"^0.8.0\",\n    \"@nodepack/plugin-express\": \"^0.8.0\",\n    \"@nodepack/plugin-passport\": \"^0.8.0\",\n    \"@nodepack/plugin-typescript\": \"^0.8.0\",\n    \"@nodepack/service\": \"^0.8.0\",\n    \"@types/algoliasearch\": \"^3.34.3\",\n    \"@types/graphql-type-json\": \"^0.3.2\",\n    \"@types/morgan\": \"^1.7.37\",\n    \"@types/ms\": \"^0.7.31\",\n    \"@types/node-fetch\": \"^2\",\n    \"@types/npm-registry-fetch\": \"^4.0.1\",\n    \"@types/passport-github2\": \"^1.2.4\"\n  },\n  \"engines\": {\n    \"node\": \">=10\"\n  }\n}\n"
  },
  {
    "path": "packages/backend/schema-fragment-matcher.js",
    "content": "\n      export default {\n  \"__schema\": {\n    \"types\": [\n      {\n        \"kind\": \"INTERFACE\",\n        \"name\": \"PackageInterface\",\n        \"possibleTypes\": [\n          {\n            \"name\": \"PackageProposal\"\n          },\n          {\n            \"name\": \"Package\"\n          }\n        ]\n      }\n    ]\n  }\n}\n    "
  },
  {
    "path": "packages/backend/src/const/error-codes.ts",
    "content": "export enum ErrorCode {\n  ERROR_GUEST = 'guest',\n  ERROR_UNAUTHORIZED = 'unauthorized',\n  ERROR_VALIDATION = 'validation',\n}\n"
  },
  {
    "path": "packages/backend/src/context/algolia.ts",
    "content": "import Algolia, { Client as AlgoliaClient } from 'algoliasearch'\nimport { onCreate, addProp } from '@nodepack/app-context'\nimport { Context } from '@/context'\n\nonCreate((ctx: Context) => {\n  addProp(ctx, 'algolia', () => Algolia(ctx.config.algolia.id, ctx.config.algolia.key))\n})\n\nexport default interface AlgoliaContext {\n  algolia: AlgoliaClient\n}\n"
  },
  {
    "path": "packages/backend/src/context/fauna.ts",
    "content": "import { onCreate, addProp } from '@nodepack/app-context'\nimport { Client as FaunaClient } from 'faunadb'\nimport { Context } from '../context'\n\nonCreate((context: Context) => {\n  addProp(context, 'db', () => new FaunaClient(context.config.db))\n})\n\nexport default interface FaunaContext {\n  db: FaunaClient\n}\n"
  },
  {
    "path": "packages/backend/src/context/github.ts",
    "content": "import { onCreate, addProp } from '@nodepack/app-context'\nimport { Context } from '../context'\nimport Octokit from '@octokit/rest'\n\nonCreate(async (context: Context) => {\n  addProp(context, 'github', () => new Octokit(context.config.github))\n})\n\nexport default interface GitHubContext {\n  github: Octokit\n}\n"
  },
  {
    "path": "packages/backend/src/context/npm.ts",
    "content": "import { onCreate, addProp } from '@nodepack/app-context'\nimport { Context } from '../context'\nimport npmFetch, { Options } from 'npm-registry-fetch'\nimport mem from 'p-memoize'\nimport ms from 'ms'\n\ntype NpmFunction = (url: string, opts?: Options) => Promise<any>\n\nonCreate(async (context: Context) => {\n  addProp(context, 'npm', () => mem(npmFetch.json, {\n    maxAge: ms('1s'),\n  }))\n  addProp(context, 'npmApi', () => mem((url: string, opts: any = undefined) => npmFetch.json(`https://api.npmjs.org/${url}`, opts), {\n    maxAge: ms('1s'),\n  }))\n})\n\nexport default interface NpmContext {\n  npm: NpmFunction\n  npmApi: NpmFunction\n}\n"
  },
  {
    "path": "packages/backend/src/context.d.ts",
    "content": "import BaseContext from '@context'\nimport { PassportUser } from '@nodepack/plugin-passport'\nimport { values } from 'faunadb'\nimport { User as BaseUser, UserAccount } from '@/generated/schema'\nimport { DBUserAccount, DBUser } from './schema/user/db-types'\n\nexport interface User extends PassportUser, BaseUser, DBUser {\n}\n\nexport interface Context extends BaseContext {\n  user: User\n}\n"
  },
  {
    "path": "packages/backend/src/generated/config.d.ts",
    "content": "// This file is auto-generated by @nodepack/plugin-typescript\n \n/* eslint-disable */\n/* tslint:disable */\n\nimport ConfigAlgolia from '@config/algolia'\nimport ConfigApollo from '@config/apollo'\nimport ConfigCookie from '@config/cookie'\nimport ConfigCors from '@config/cors'\nimport ConfigDb from '@config/db'\nimport ConfigGithub from '@config/github'\n\nexport default interface BaseConfig {\n  algolia: typeof ConfigAlgolia\n  apollo: typeof ConfigApollo\n  cookie: typeof ConfigCookie\n  cors: typeof ConfigCors\n  db: typeof ConfigDb\n  github: typeof ConfigGithub\n}\n\nexport type Config = BaseConfig\n"
  },
  {
    "path": "packages/backend/src/generated/context.d.ts",
    "content": "// This file is auto-generated by @nodepack/plugin-typescript\n \n/* eslint-disable */\n/* tslint:disable */\n\n// Config\nimport ProjectConfigBase from './config'\n// Plugins\nimport NodepackPluginApollo from '@nodepack/plugin-apollo/types/context'\nimport NodepackPluginExpress from '@nodepack/plugin-express/types/context'\nimport NodepackPluginPassport from '@nodepack/plugin-passport/types/context'\n// Project context files\nimport Algolia from '@/context/algolia'\nimport Fauna from '@/context/fauna'\nimport Github from '@/context/github'\nimport Npm from '@/context/npm'\n\ninterface ContextBase {\n  config: ProjectConfigBase\n}\n\nexport type Context = ContextBase\n  & NodepackPluginApollo\n  & NodepackPluginExpress\n  & NodepackPluginPassport\n  & Algolia\n  & Fauna\n  & Github\n  & Npm\n\nexport default Context\n"
  },
  {
    "path": "packages/backend/src/generated/schema.ts",
    "content": "import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';\nimport { DBPackageInterface } from '@/schema/package-interface/db-types';\nimport { DBPackageProposal } from '@/schema/package-proposal/db-types';\nimport { DBProjectType } from '@/schema/project-type/db-types';\nimport { DBUser, DBUserAccount } from '@/schema/user/db-types';\nimport { Context } from '@/context';\nexport type Maybe<T> = T | null;\nexport type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;\nexport type RequireFields<T, K extends keyof T> = { [X in Exclude<keyof T, K>]?: T[X] } & { [P in K]-?: NonNullable<T[P]> };\n/** All built-in and custom scalars, mapped to their actual values */\nexport type Scalars = {\n  ID: string,\n  String: string,\n  Boolean: boolean,\n  Int: number,\n  Float: number,\n  JSON: any,\n  Date: any,\n};\n\n\n\nexport type ApprovePackageProposalInput = {\n  proposalId: Scalars['ID'],\n};\n\nexport type CreateTeamInput = {\n  name: Scalars['String'],\n  projectTypeIds: Array<Scalars['ID']>,\n  userIds: Array<Scalars['ID']>,\n};\n\nexport type DataSourcesInput = {\n  github?: Maybe<GithubDataSourceInput>,\n  npm?: Maybe<NpmDataSourceInput>,\n};\n\n\nexport type DownloadsPoint = {\n   __typename?: 'DownloadsPoint',\n  downloads: Scalars['Int'],\n  day: Scalars['Date'],\n};\n\nexport type EditPackageInfoInput = {\n  common: EditPackageInterfaceInput,\n};\n\nexport type EditPackageInterfaceInput = {\n  id: Scalars['ID'],\n  info?: Maybe<PackageInfoInput>,\n  dataSources?: Maybe<DataSourcesInput>,\n};\n\nexport type EditPackageProjectTypesInput = {\n  packageId: Scalars['ID'],\n  projectTypeIds: Array<Scalars['ID']>,\n};\n\nexport type EditPackageProposalInfoInput = {\n  common: EditPackageInterfaceInput,\n};\n\nexport type EditTeamInput = {\n  id: Scalars['ID'],\n  name: Scalars['String'],\n  projectTypeIds: Array<Scalars['ID']>,\n  userIds: Array<Scalars['ID']>,\n};\n\nexport type GithubDataSourceInput = {\n  owner: Scalars['String'],\n  repo: Scalars['String'],\n};\n\n\nexport type Mutation = {\n   __typename?: 'Mutation',\n  togglePackageBookmark?: Maybe<Package>,\n  indexPackages?: Maybe<Scalars['Boolean']>,\n  indexPackage?: Maybe<Scalars['Boolean']>,\n  resetProjectTypeTagCounters?: Maybe<Scalars['Boolean']>,\n  approvePackageProposal?: Maybe<Package>,\n  editPackageProposalProjectTypes?: Maybe<PackageProposal>,\n  editPackageProposalInfo?: Maybe<PackageProposal>,\n  proposePackage?: Maybe<PackageProposal>,\n  removePackageProposal: Scalars['Boolean'],\n  togglePackageProposalUpvote?: Maybe<PackageProposal>,\n  editPackageProjectTypes?: Maybe<Package>,\n  editPackageInfo?: Maybe<Package>,\n  toggleProjectTypeBookmark?: Maybe<ProjectType>,\n  createTeam?: Maybe<Team>,\n  editTeam?: Maybe<Team>,\n};\n\n\nexport type MutationTogglePackageBookmarkArgs = {\n  input: TogglePackageBookmarkInput\n};\n\n\nexport type MutationIndexPackageArgs = {\n  id: Scalars['ID']\n};\n\n\nexport type MutationApprovePackageProposalArgs = {\n  input: ApprovePackageProposalInput\n};\n\n\nexport type MutationEditPackageProposalProjectTypesArgs = {\n  input: EditPackageProjectTypesInput\n};\n\n\nexport type MutationEditPackageProposalInfoArgs = {\n  input: EditPackageProposalInfoInput\n};\n\n\nexport type MutationProposePackageArgs = {\n  input: ProposePackageInput\n};\n\n\nexport type MutationRemovePackageProposalArgs = {\n  input: RemovePackageProposalInput\n};\n\n\nexport type MutationTogglePackageProposalUpvoteArgs = {\n  input: TogglePackageProposalUpvoteInput\n};\n\n\nexport type MutationEditPackageProjectTypesArgs = {\n  input: EditPackageProjectTypesInput\n};\n\n\nexport type MutationEditPackageInfoArgs = {\n  input: EditPackageInfoInput\n};\n\n\nexport type MutationToggleProjectTypeBookmarkArgs = {\n  input: ToggleProjectTypeBookmarkInput\n};\n\n\nexport type MutationCreateTeamArgs = {\n  input: CreateTeamInput\n};\n\n\nexport type MutationEditTeamArgs = {\n  input: EditTeamInput\n};\n\nexport type NpmDataSourceInput = {\n  name: Scalars['String'],\n};\n\nexport type Package = PackageInterface & {\n   __typename?: 'Package',\n  id: Scalars['ID'],\n  name: Scalars['String'],\n  projectTypes: Array<ProjectType>,\n  info: PackageInfo,\n  dataSources: Array<PackageDataSource>,\n  insight: PackageInsight,\n  stars?: Maybe<Scalars['Int']>,\n  repo?: Maybe<Scalars['String']>,\n  defaultLogo?: Maybe<Scalars['String']>,\n  maintainers: Array<PackageMaintainer>,\n  homepage?: Maybe<Scalars['String']>,\n  license?: Maybe<Scalars['String']>,\n  description?: Maybe<Scalars['String']>,\n  readme?: Maybe<Scalars['String']>,\n  releases: Array<PackageRelease>,\n  releaseCount?: Maybe<Scalars['Int']>,\n  tagCount?: Maybe<Scalars['Int']>,\n  bookmarked?: Maybe<Scalars['Boolean']>,\n};\n\nexport type PackageDataSource = {\n   __typename?: 'PackageDataSource',\n  type: Scalars['String'],\n  data?: Maybe<Scalars['JSON']>,\n};\n\nexport type PackageInfo = {\n   __typename?: 'PackageInfo',\n  tags: Array<Scalars['String']>,\n};\n\nexport type PackageInfoInput = {\n  tags: Array<Scalars['String']>,\n};\n\nexport type PackageInsight = {\n   __typename?: 'PackageInsight',\n  npm?: Maybe<PackageNpmInsight>,\n};\n\nexport type PackageInterface = {\n  id: Scalars['ID'],\n  name: Scalars['String'],\n  projectTypes: Array<ProjectType>,\n  info: PackageInfo,\n  dataSources: Array<PackageDataSource>,\n  insight: PackageInsight,\n  stars?: Maybe<Scalars['Int']>,\n  repo?: Maybe<Scalars['String']>,\n  defaultLogo?: Maybe<Scalars['String']>,\n  maintainers: Array<PackageMaintainer>,\n  homepage?: Maybe<Scalars['String']>,\n  license?: Maybe<Scalars['String']>,\n  description?: Maybe<Scalars['String']>,\n  readme?: Maybe<Scalars['String']>,\n  releases: Array<PackageRelease>,\n  releaseCount?: Maybe<Scalars['Int']>,\n  tagCount?: Maybe<Scalars['Int']>,\n};\n\nexport type PackageMaintainer = {\n   __typename?: 'PackageMaintainer',\n  name?: Maybe<Scalars['String']>,\n  email?: Maybe<Scalars['String']>,\n  avatar?: Maybe<Scalars['String']>,\n};\n\nexport type PackageNpmInsight = {\n   __typename?: 'PackageNpmInsight',\n  downloads: Scalars['Int'],\n  downloadsPoints: Array<DownloadsPoint>,\n};\n\n\nexport type PackageNpmInsightDownloadsArgs = {\n  range: PackageNpmInsightDownloadsRange\n};\n\n\nexport type PackageNpmInsightDownloadsPointsArgs = {\n  range: PackageNpmInsightDownloadsRange\n};\n\nexport enum PackageNpmInsightDownloadsRange {\n  Day = 'day',\n  Week = 'week',\n  Month = 'month'\n}\n\nexport type PackageProposal = PackageInterface & {\n   __typename?: 'PackageProposal',\n  id: Scalars['ID'],\n  name: Scalars['String'],\n  projectTypes: Array<ProjectType>,\n  info: PackageInfo,\n  dataSources: Array<PackageDataSource>,\n  insight: PackageInsight,\n  stars?: Maybe<Scalars['Int']>,\n  repo?: Maybe<Scalars['String']>,\n  defaultLogo?: Maybe<Scalars['String']>,\n  maintainers: Array<PackageMaintainer>,\n  homepage?: Maybe<Scalars['String']>,\n  license?: Maybe<Scalars['String']>,\n  description?: Maybe<Scalars['String']>,\n  readme?: Maybe<Scalars['String']>,\n  releases: Array<PackageRelease>,\n  releaseCount?: Maybe<Scalars['Int']>,\n  tagCount?: Maybe<Scalars['Int']>,\n  user?: Maybe<User>,\n  upvotes: Scalars['Int'],\n  upvoted: Scalars['Boolean'],\n};\n\nexport type PackageRelease = {\n   __typename?: 'PackageRelease',\n  id: Scalars['ID'],\n  date?: Maybe<Scalars['Date']>,\n  title?: Maybe<Scalars['String']>,\n  tagName?: Maybe<Scalars['String']>,\n  description?: Maybe<Scalars['String']>,\n  prerelease?: Maybe<Scalars['Boolean']>,\n  assets: Array<PackageReleaseAsset>,\n};\n\nexport type PackageReleaseAsset = {\n   __typename?: 'PackageReleaseAsset',\n  name: Scalars['String'],\n  downloadUrl: Scalars['String'],\n  size: Scalars['Int'],\n};\n\nexport type PackagesPage = {\n   __typename?: 'PackagesPage',\n  items: Array<Package>,\n  after?: Maybe<Scalars['JSON']>,\n};\n\nexport type ProjectType = {\n   __typename?: 'ProjectType',\n  id: Scalars['ID'],\n  name: Scalars['String'],\n  slug: Scalars['String'],\n  logo: Scalars['String'],\n  popularTags: Array<Tag>,\n  tags: Array<Tag>,\n  inTeam: Scalars['Boolean'],\n  packageProposals: Array<PackageProposal>,\n  packageProposalCount: Scalars['Int'],\n  packages: PackagesPage,\n  bookmarked?: Maybe<Scalars['Boolean']>,\n};\n\n\nexport type ProjectTypePackagesArgs = {\n  tags?: Maybe<Array<Scalars['String']>>,\n  after?: Maybe<Scalars['JSON']>\n};\n\nexport type ProposePackageInput = {\n  projectTypeId: Scalars['ID'],\n  packageName: Scalars['String'],\n  tags: Array<Scalars['String']>,\n};\n\nexport type Query = {\n   __typename?: 'Query',\n  currentUser?: Maybe<User>,\n  allUsers: Array<User>,\n  packageProposal?: Maybe<PackageProposal>,\n  packageProposalByName?: Maybe<PackageProposal>,\n  package?: Maybe<Package>,\n  packageByName?: Maybe<Package>,\n  projectTypes: Array<ProjectType>,\n  projectType?: Maybe<ProjectType>,\n  projectTypeBySlug?: Maybe<ProjectType>,\n  team?: Maybe<Team>,\n  allTeams: Array<Team>,\n};\n\n\nexport type QueryPackageProposalArgs = {\n  id: Scalars['ID']\n};\n\n\nexport type QueryPackageProposalByNameArgs = {\n  name: Scalars['String']\n};\n\n\nexport type QueryPackageArgs = {\n  id: Scalars['ID']\n};\n\n\nexport type QueryPackageByNameArgs = {\n  name: Scalars['String']\n};\n\n\nexport type QueryProjectTypeArgs = {\n  id: Scalars['ID']\n};\n\n\nexport type QueryProjectTypeBySlugArgs = {\n  slug: Scalars['String']\n};\n\n\nexport type QueryTeamArgs = {\n  id: Scalars['ID']\n};\n\nexport type RemovePackageProposalInput = {\n  id: Scalars['ID'],\n};\n\nexport type Tag = {\n   __typename?: 'Tag',\n  id: Scalars['ID'],\n  count: Scalars['Int'],\n};\n\nexport type Team = {\n   __typename?: 'Team',\n  id: Scalars['ID'],\n  name: Scalars['String'],\n  projectTypes: Array<ProjectType>,\n  users: Array<User>,\n};\n\nexport type TogglePackageBookmarkInput = {\n  packageId: Scalars['ID'],\n};\n\nexport type TogglePackageProposalUpvoteInput = {\n  proposalId: Scalars['ID'],\n};\n\nexport type ToggleProjectTypeBookmarkInput = {\n  projectTypeId: Scalars['ID'],\n};\n\nexport type User = {\n   __typename?: 'User',\n  id: Scalars['ID'],\n  nickname: Scalars['String'],\n  email: Scalars['String'],\n  accounts: Array<UserAccount>,\n  avatar?: Maybe<Scalars['String']>,\n  admin?: Maybe<Scalars['Boolean']>,\n  teams: Array<Team>,\n  bookmarkedPackages: Array<Package>,\n};\n\nexport type UserAccount = {\n   __typename?: 'UserAccount',\n  id: Scalars['ID'],\n  provider: Scalars['String'],\n  profileId: Scalars['ID'],\n  nickname?: Maybe<Scalars['String']>,\n  profileUrl?: Maybe<Scalars['String']>,\n};\n\n\n\nexport type ResolverTypeWrapper<T> = Promise<T> | T;\n\nexport type ResolverFn<TResult, TParent, TContext, TArgs> = (\n  parent: TParent,\n  args: TArgs,\n  context: TContext,\n  info: GraphQLResolveInfo\n) => Promise<TResult> | TResult;\n\n\nexport type StitchingResolver<TResult, TParent, TContext, TArgs> = {\n  fragment: string;\n  resolve: ResolverFn<TResult, TParent, TContext, TArgs>;\n};\n\nexport type Resolver<TResult, TParent = {}, TContext = {}, TArgs = {}> =\n  | ResolverFn<TResult, TParent, TContext, TArgs>\n  | StitchingResolver<TResult, TParent, TContext, TArgs>;\n\nexport type SubscriptionSubscribeFn<TResult, TParent, TContext, TArgs> = (\n  parent: TParent,\n  args: TArgs,\n  context: TContext,\n  info: GraphQLResolveInfo\n) => AsyncIterator<TResult> | Promise<AsyncIterator<TResult>>;\n\nexport type SubscriptionResolveFn<TResult, TParent, TContext, TArgs> = (\n  parent: TParent,\n  args: TArgs,\n  context: TContext,\n  info: GraphQLResolveInfo\n) => TResult | Promise<TResult>;\n\nexport interface SubscriptionSubscriberObject<TResult, TKey extends string, TParent, TContext, TArgs> {\n  subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>;\n  resolve?: SubscriptionResolveFn<TResult, { [key in TKey]: TResult }, TContext, TArgs>;\n}\n\nexport interface SubscriptionResolverObject<TResult, TParent, TContext, TArgs> {\n  subscribe: SubscriptionSubscribeFn<any, TParent, TContext, TArgs>;\n  resolve: SubscriptionResolveFn<TResult, any, TContext, TArgs>;\n}\n\nexport type SubscriptionObject<TResult, TKey extends string, TParent, TContext, TArgs> =\n  | SubscriptionSubscriberObject<TResult, TKey, TParent, TContext, TArgs>\n  | SubscriptionResolverObject<TResult, TParent, TContext, TArgs>;\n\nexport type SubscriptionResolver<TResult, TKey extends string, TParent = {}, TContext = {}, TArgs = {}> =\n  | ((...args: any[]) => SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>)\n  | SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>;\n\nexport type TypeResolveFn<TTypes, TParent = {}, TContext = {}> = (\n  parent: TParent,\n  context: TContext,\n  info: GraphQLResolveInfo\n) => Maybe<TTypes>;\n\nexport type NextResolverFn<T> = () => Promise<T>;\n\nexport type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs = {}> = (\n  next: NextResolverFn<TResult>,\n  parent: TParent,\n  args: TArgs,\n  context: TContext,\n  info: GraphQLResolveInfo\n) => TResult | Promise<TResult>;\n\n/** Mapping between all available schema types and the resolvers types */\nexport type ResolversTypes = {\n  Query: ResolverTypeWrapper<{}>,\n  User: ResolverTypeWrapper<DBUser>,\n  ID: ResolverTypeWrapper<Scalars['ID']>,\n  String: ResolverTypeWrapper<Scalars['String']>,\n  UserAccount: ResolverTypeWrapper<DBUserAccount>,\n  Boolean: ResolverTypeWrapper<Scalars['Boolean']>,\n  Team: ResolverTypeWrapper<Omit<Team, 'projectTypes' | 'users'> & { projectTypes: Array<ResolversTypes['ProjectType']>, users: Array<ResolversTypes['User']> }>,\n  ProjectType: ResolverTypeWrapper<DBProjectType>,\n  Tag: ResolverTypeWrapper<Tag>,\n  Int: ResolverTypeWrapper<Scalars['Int']>,\n  PackageProposal: ResolverTypeWrapper<DBPackageProposal>,\n  PackageInterface: ResolverTypeWrapper<DBPackageInterface>,\n  PackageInfo: ResolverTypeWrapper<PackageInfo>,\n  PackageDataSource: ResolverTypeWrapper<PackageDataSource>,\n  JSON: ResolverTypeWrapper<Scalars['JSON']>,\n  PackageInsight: ResolverTypeWrapper<PackageInsight>,\n  PackageNpmInsight: ResolverTypeWrapper<PackageNpmInsight>,\n  PackageNpmInsightDownloadsRange: PackageNpmInsightDownloadsRange,\n  DownloadsPoint: ResolverTypeWrapper<DownloadsPoint>,\n  Date: ResolverTypeWrapper<Scalars['Date']>,\n  PackageMaintainer: ResolverTypeWrapper<PackageMaintainer>,\n  PackageRelease: ResolverTypeWrapper<PackageRelease>,\n  PackageReleaseAsset: ResolverTypeWrapper<PackageReleaseAsset>,\n  PackagesPage: ResolverTypeWrapper<Omit<PackagesPage, 'items'> & { items: Array<ResolversTypes['Package']> }>,\n  Package: ResolverTypeWrapper<Omit<Package, 'projectTypes'> & { projectTypes: Array<ResolversTypes['ProjectType']> }>,\n  Mutation: ResolverTypeWrapper<{}>,\n  TogglePackageBookmarkInput: TogglePackageBookmarkInput,\n  ApprovePackageProposalInput: ApprovePackageProposalInput,\n  EditPackageProjectTypesInput: EditPackageProjectTypesInput,\n  EditPackageProposalInfoInput: EditPackageProposalInfoInput,\n  EditPackageInterfaceInput: EditPackageInterfaceInput,\n  PackageInfoInput: PackageInfoInput,\n  DataSourcesInput: DataSourcesInput,\n  GithubDataSourceInput: GithubDataSourceInput,\n  NpmDataSourceInput: NpmDataSourceInput,\n  ProposePackageInput: ProposePackageInput,\n  RemovePackageProposalInput: RemovePackageProposalInput,\n  TogglePackageProposalUpvoteInput: TogglePackageProposalUpvoteInput,\n  EditPackageInfoInput: EditPackageInfoInput,\n  ToggleProjectTypeBookmarkInput: ToggleProjectTypeBookmarkInput,\n  CreateTeamInput: CreateTeamInput,\n  EditTeamInput: EditTeamInput,\n};\n\n/** Mapping between all available schema types and the resolvers parents */\nexport type ResolversParentTypes = {\n  Query: {},\n  User: DBUser,\n  ID: Scalars['ID'],\n  String: Scalars['String'],\n  UserAccount: DBUserAccount,\n  Boolean: Scalars['Boolean'],\n  Team: Omit<Team, 'projectTypes' | 'users'> & { projectTypes: Array<ResolversParentTypes['ProjectType']>, users: Array<ResolversParentTypes['User']> },\n  ProjectType: DBProjectType,\n  Tag: Tag,\n  Int: Scalars['Int'],\n  PackageProposal: DBPackageProposal,\n  PackageInterface: DBPackageInterface,\n  PackageInfo: PackageInfo,\n  PackageDataSource: PackageDataSource,\n  JSON: Scalars['JSON'],\n  PackageInsight: PackageInsight,\n  PackageNpmInsight: PackageNpmInsight,\n  PackageNpmInsightDownloadsRange: PackageNpmInsightDownloadsRange,\n  DownloadsPoint: DownloadsPoint,\n  Date: Scalars['Date'],\n  PackageMaintainer: PackageMaintainer,\n  PackageRelease: PackageRelease,\n  PackageReleaseAsset: PackageReleaseAsset,\n  PackagesPage: Omit<PackagesPage, 'items'> & { items: Array<ResolversParentTypes['Package']> },\n  Package: Omit<Package, 'projectTypes'> & { projectTypes: Array<ResolversParentTypes['ProjectType']> },\n  Mutation: {},\n  TogglePackageBookmarkInput: TogglePackageBookmarkInput,\n  ApprovePackageProposalInput: ApprovePackageProposalInput,\n  EditPackageProjectTypesInput: EditPackageProjectTypesInput,\n  EditPackageProposalInfoInput: EditPackageProposalInfoInput,\n  EditPackageInterfaceInput: EditPackageInterfaceInput,\n  PackageInfoInput: PackageInfoInput,\n  DataSourcesInput: DataSourcesInput,\n  GithubDataSourceInput: GithubDataSourceInput,\n  NpmDataSourceInput: NpmDataSourceInput,\n  ProposePackageInput: ProposePackageInput,\n  RemovePackageProposalInput: RemovePackageProposalInput,\n  TogglePackageProposalUpvoteInput: TogglePackageProposalUpvoteInput,\n  EditPackageInfoInput: EditPackageInfoInput,\n  ToggleProjectTypeBookmarkInput: ToggleProjectTypeBookmarkInput,\n  CreateTeamInput: CreateTeamInput,\n  EditTeamInput: EditTeamInput,\n};\n\nexport type AdminDirectiveResolver<Result, Parent, ContextType = Context, Args = {  }> = DirectiveResolverFn<Result, Parent, ContextType, Args>;\n\nexport type AuthDirectiveResolver<Result, Parent, ContextType = Context, Args = {  }> = DirectiveResolverFn<Result, Parent, ContextType, Args>;\n\nexport interface DateScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['Date'], any> {\n  name: 'Date'\n}\n\nexport type DownloadsPointResolvers<ContextType = Context, ParentType extends ResolversParentTypes['DownloadsPoint'] = ResolversParentTypes['DownloadsPoint']> = {\n  downloads?: Resolver<ResolversTypes['Int'], ParentType, ContextType>,\n  day?: Resolver<ResolversTypes['Date'], ParentType, ContextType>,\n};\n\nexport interface JsonScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['JSON'], any> {\n  name: 'JSON'\n}\n\nexport type MutationResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation']> = {\n  togglePackageBookmark?: Resolver<Maybe<ResolversTypes['Package']>, ParentType, ContextType, RequireFields<MutationTogglePackageBookmarkArgs, 'input'>>,\n  indexPackages?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>,\n  indexPackage?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType, RequireFields<MutationIndexPackageArgs, 'id'>>,\n  resetProjectTypeTagCounters?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>,\n  approvePackageProposal?: Resolver<Maybe<ResolversTypes['Package']>, ParentType, ContextType, RequireFields<MutationApprovePackageProposalArgs, 'input'>>,\n  editPackageProposalProjectTypes?: Resolver<Maybe<ResolversTypes['PackageProposal']>, ParentType, ContextType, RequireFields<MutationEditPackageProposalProjectTypesArgs, 'input'>>,\n  editPackageProposalInfo?: Resolver<Maybe<ResolversTypes['PackageProposal']>, ParentType, ContextType, RequireFields<MutationEditPackageProposalInfoArgs, 'input'>>,\n  proposePackage?: Resolver<Maybe<ResolversTypes['PackageProposal']>, ParentType, ContextType, RequireFields<MutationProposePackageArgs, 'input'>>,\n  removePackageProposal?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationRemovePackageProposalArgs, 'input'>>,\n  togglePackageProposalUpvote?: Resolver<Maybe<ResolversTypes['PackageProposal']>, ParentType, ContextType, RequireFields<MutationTogglePackageProposalUpvoteArgs, 'input'>>,\n  editPackageProjectTypes?: Resolver<Maybe<ResolversTypes['Package']>, ParentType, ContextType, RequireFields<MutationEditPackageProjectTypesArgs, 'input'>>,\n  editPackageInfo?: Resolver<Maybe<ResolversTypes['Package']>, ParentType, ContextType, RequireFields<MutationEditPackageInfoArgs, 'input'>>,\n  toggleProjectTypeBookmark?: Resolver<Maybe<ResolversTypes['ProjectType']>, ParentType, ContextType, RequireFields<MutationToggleProjectTypeBookmarkArgs, 'input'>>,\n  createTeam?: Resolver<Maybe<ResolversTypes['Team']>, ParentType, ContextType, RequireFields<MutationCreateTeamArgs, 'input'>>,\n  editTeam?: Resolver<Maybe<ResolversTypes['Team']>, ParentType, ContextType, RequireFields<MutationEditTeamArgs, 'input'>>,\n};\n\nexport type PackageResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Package'] = ResolversParentTypes['Package']> = {\n  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>,\n  name?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  projectTypes?: Resolver<Array<ResolversTypes['ProjectType']>, ParentType, ContextType>,\n  info?: Resolver<ResolversTypes['PackageInfo'], ParentType, ContextType>,\n  dataSources?: Resolver<Array<ResolversTypes['PackageDataSource']>, ParentType, ContextType>,\n  insight?: Resolver<ResolversTypes['PackageInsight'], ParentType, ContextType>,\n  stars?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>,\n  repo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  defaultLogo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  maintainers?: Resolver<Array<ResolversTypes['PackageMaintainer']>, ParentType, ContextType>,\n  homepage?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  license?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  readme?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  releases?: Resolver<Array<ResolversTypes['PackageRelease']>, ParentType, ContextType>,\n  releaseCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>,\n  tagCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>,\n  bookmarked?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>,\n};\n\nexport type PackageDataSourceResolvers<ContextType = Context, ParentType extends ResolversParentTypes['PackageDataSource'] = ResolversParentTypes['PackageDataSource']> = {\n  type?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  data?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>,\n};\n\nexport type PackageInfoResolvers<ContextType = Context, ParentType extends ResolversParentTypes['PackageInfo'] = ResolversParentTypes['PackageInfo']> = {\n  tags?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>,\n};\n\nexport type PackageInsightResolvers<ContextType = Context, ParentType extends ResolversParentTypes['PackageInsight'] = ResolversParentTypes['PackageInsight']> = {\n  npm?: Resolver<Maybe<ResolversTypes['PackageNpmInsight']>, ParentType, ContextType>,\n};\n\nexport type PackageInterfaceResolvers<ContextType = Context, ParentType extends ResolversParentTypes['PackageInterface'] = ResolversParentTypes['PackageInterface']> = {\n  __resolveType?: TypeResolveFn<'PackageProposal' | 'Package', ParentType, ContextType>,\n  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>,\n  name?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  projectTypes?: Resolver<Array<ResolversTypes['ProjectType']>, ParentType, ContextType>,\n  info?: Resolver<ResolversTypes['PackageInfo'], ParentType, ContextType>,\n  dataSources?: Resolver<Array<ResolversTypes['PackageDataSource']>, ParentType, ContextType>,\n  insight?: Resolver<ResolversTypes['PackageInsight'], ParentType, ContextType>,\n  stars?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>,\n  repo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  defaultLogo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  maintainers?: Resolver<Array<ResolversTypes['PackageMaintainer']>, ParentType, ContextType>,\n  homepage?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  license?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  readme?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  releases?: Resolver<Array<ResolversTypes['PackageRelease']>, ParentType, ContextType>,\n  releaseCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>,\n  tagCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>,\n};\n\nexport type PackageMaintainerResolvers<ContextType = Context, ParentType extends ResolversParentTypes['PackageMaintainer'] = ResolversParentTypes['PackageMaintainer']> = {\n  name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  email?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  avatar?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n};\n\nexport type PackageNpmInsightResolvers<ContextType = Context, ParentType extends ResolversParentTypes['PackageNpmInsight'] = ResolversParentTypes['PackageNpmInsight']> = {\n  downloads?: Resolver<ResolversTypes['Int'], ParentType, ContextType, RequireFields<PackageNpmInsightDownloadsArgs, 'range'>>,\n  downloadsPoints?: Resolver<Array<ResolversTypes['DownloadsPoint']>, ParentType, ContextType, RequireFields<PackageNpmInsightDownloadsPointsArgs, 'range'>>,\n};\n\nexport type PackageProposalResolvers<ContextType = Context, ParentType extends ResolversParentTypes['PackageProposal'] = ResolversParentTypes['PackageProposal']> = {\n  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>,\n  name?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  projectTypes?: Resolver<Array<ResolversTypes['ProjectType']>, ParentType, ContextType>,\n  info?: Resolver<ResolversTypes['PackageInfo'], ParentType, ContextType>,\n  dataSources?: Resolver<Array<ResolversTypes['PackageDataSource']>, ParentType, ContextType>,\n  insight?: Resolver<ResolversTypes['PackageInsight'], ParentType, ContextType>,\n  stars?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>,\n  repo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  defaultLogo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  maintainers?: Resolver<Array<ResolversTypes['PackageMaintainer']>, ParentType, ContextType>,\n  homepage?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  license?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  readme?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  releases?: Resolver<Array<ResolversTypes['PackageRelease']>, ParentType, ContextType>,\n  releaseCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>,\n  tagCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>,\n  user?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>,\n  upvotes?: Resolver<ResolversTypes['Int'], ParentType, ContextType>,\n  upvoted?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>,\n};\n\nexport type PackageReleaseResolvers<ContextType = Context, ParentType extends ResolversParentTypes['PackageRelease'] = ResolversParentTypes['PackageRelease']> = {\n  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>,\n  date?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>,\n  title?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  tagName?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  prerelease?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>,\n  assets?: Resolver<Array<ResolversTypes['PackageReleaseAsset']>, ParentType, ContextType>,\n};\n\nexport type PackageReleaseAssetResolvers<ContextType = Context, ParentType extends ResolversParentTypes['PackageReleaseAsset'] = ResolversParentTypes['PackageReleaseAsset']> = {\n  name?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  downloadUrl?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  size?: Resolver<ResolversTypes['Int'], ParentType, ContextType>,\n};\n\nexport type PackagesPageResolvers<ContextType = Context, ParentType extends ResolversParentTypes['PackagesPage'] = ResolversParentTypes['PackagesPage']> = {\n  items?: Resolver<Array<ResolversTypes['Package']>, ParentType, ContextType>,\n  after?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>,\n};\n\nexport type ProjectTypeResolvers<ContextType = Context, ParentType extends ResolversParentTypes['ProjectType'] = ResolversParentTypes['ProjectType']> = {\n  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>,\n  name?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  slug?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  logo?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  popularTags?: Resolver<Array<ResolversTypes['Tag']>, ParentType, ContextType>,\n  tags?: Resolver<Array<ResolversTypes['Tag']>, ParentType, ContextType>,\n  inTeam?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>,\n  packageProposals?: Resolver<Array<ResolversTypes['PackageProposal']>, ParentType, ContextType>,\n  packageProposalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>,\n  packages?: Resolver<ResolversTypes['PackagesPage'], ParentType, ContextType, RequireFields<ProjectTypePackagesArgs, 'tags' | 'after'>>,\n  bookmarked?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>,\n};\n\nexport type QueryResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = {\n  currentUser?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>,\n  allUsers?: Resolver<Array<ResolversTypes['User']>, ParentType, ContextType>,\n  packageProposal?: Resolver<Maybe<ResolversTypes['PackageProposal']>, ParentType, ContextType, RequireFields<QueryPackageProposalArgs, 'id'>>,\n  packageProposalByName?: Resolver<Maybe<ResolversTypes['PackageProposal']>, ParentType, ContextType, RequireFields<QueryPackageProposalByNameArgs, 'name'>>,\n  package?: Resolver<Maybe<ResolversTypes['Package']>, ParentType, ContextType, RequireFields<QueryPackageArgs, 'id'>>,\n  packageByName?: Resolver<Maybe<ResolversTypes['Package']>, ParentType, ContextType, RequireFields<QueryPackageByNameArgs, 'name'>>,\n  projectTypes?: Resolver<Array<ResolversTypes['ProjectType']>, ParentType, ContextType>,\n  projectType?: Resolver<Maybe<ResolversTypes['ProjectType']>, ParentType, ContextType, RequireFields<QueryProjectTypeArgs, 'id'>>,\n  projectTypeBySlug?: Resolver<Maybe<ResolversTypes['ProjectType']>, ParentType, ContextType, RequireFields<QueryProjectTypeBySlugArgs, 'slug'>>,\n  team?: Resolver<Maybe<ResolversTypes['Team']>, ParentType, ContextType, RequireFields<QueryTeamArgs, 'id'>>,\n  allTeams?: Resolver<Array<ResolversTypes['Team']>, ParentType, ContextType>,\n};\n\nexport type TagResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Tag'] = ResolversParentTypes['Tag']> = {\n  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>,\n  count?: Resolver<ResolversTypes['Int'], ParentType, ContextType>,\n};\n\nexport type TeamResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Team'] = ResolversParentTypes['Team']> = {\n  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>,\n  name?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  projectTypes?: Resolver<Array<ResolversTypes['ProjectType']>, ParentType, ContextType>,\n  users?: Resolver<Array<ResolversTypes['User']>, ParentType, ContextType>,\n};\n\nexport type UserResolvers<ContextType = Context, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']> = {\n  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>,\n  nickname?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  email?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  accounts?: Resolver<Array<ResolversTypes['UserAccount']>, ParentType, ContextType>,\n  avatar?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  admin?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>,\n  teams?: Resolver<Array<ResolversTypes['Team']>, ParentType, ContextType>,\n  bookmarkedPackages?: Resolver<Array<ResolversTypes['Package']>, ParentType, ContextType>,\n};\n\nexport type UserAccountResolvers<ContextType = Context, ParentType extends ResolversParentTypes['UserAccount'] = ResolversParentTypes['UserAccount']> = {\n  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>,\n  provider?: Resolver<ResolversTypes['String'], ParentType, ContextType>,\n  profileId?: Resolver<ResolversTypes['ID'], ParentType, ContextType>,\n  nickname?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n  profileUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>,\n};\n\nexport type Resolvers<ContextType = Context> = {\n  Date?: GraphQLScalarType,\n  DownloadsPoint?: DownloadsPointResolvers<ContextType>,\n  JSON?: GraphQLScalarType,\n  Mutation?: MutationResolvers<ContextType>,\n  Package?: PackageResolvers<ContextType>,\n  PackageDataSource?: PackageDataSourceResolvers<ContextType>,\n  PackageInfo?: PackageInfoResolvers<ContextType>,\n  PackageInsight?: PackageInsightResolvers<ContextType>,\n  PackageInterface?: PackageInterfaceResolvers,\n  PackageMaintainer?: PackageMaintainerResolvers<ContextType>,\n  PackageNpmInsight?: PackageNpmInsightResolvers<ContextType>,\n  PackageProposal?: PackageProposalResolvers<ContextType>,\n  PackageRelease?: PackageReleaseResolvers<ContextType>,\n  PackageReleaseAsset?: PackageReleaseAssetResolvers<ContextType>,\n  PackagesPage?: PackagesPageResolvers<ContextType>,\n  ProjectType?: ProjectTypeResolvers<ContextType>,\n  Query?: QueryResolvers<ContextType>,\n  Tag?: TagResolvers<ContextType>,\n  Team?: TeamResolvers<ContextType>,\n  User?: UserResolvers<ContextType>,\n  UserAccount?: UserAccountResolvers<ContextType>,\n};\n\n\n/**\n * @deprecated\n * Use \"Resolvers\" root object instead. If you wish to get \"IResolvers\", add \"typesPrefix: I\" to your config.\n*/\nexport type IResolvers<ContextType = Context> = Resolvers<ContextType>;\nexport type DirectiveResolvers<ContextType = Context> = {\n  admin?: AdminDirectiveResolver<any, any, ContextType>,\n  auth?: AuthDirectiveResolver<any, any, ContextType>,\n};\n\n\n/**\n* @deprecated\n* Use \"DirectiveResolvers\" root object instead. If you wish to get \"IDirectiveResolvers\", add \"typesPrefix: I\" to your config.\n*/\nexport type IDirectiveResolvers<ContextType = Context> = DirectiveResolvers<ContextType>;"
  },
  {
    "path": "packages/backend/src/index.ts",
    "content": "import './passport'\nimport { bootstrap, printReady } from '@nodepack/app'\nimport { Context } from './context'\nimport morgan from 'morgan'\nimport { hook } from '@nodepack/app-context'\nimport { spawn } from 'child_process'\n\nif (process.env.NODEPACK_ENV === 'development') {\n  hook('expressCreate', (ctx: Context) => {\n    // Express logs\n    ctx.express.use(morgan('dev'))\n    ctx.express.use(morgan(':method :url req-cookie: :req[cookie] res-set-cookie: :res[set-cookie]'))\n  })\n}\n\nbootstrap(() => {\n  printReady()\n})\n\n// Auto-generate shcema code\nhook('apolloListen', () => {\n  spawn('yarn', ['schema-gen'], {\n    cwd: process.cwd(),\n    stdio: ['inherit', 'inherit', 'inherit'],\n  })\n})\n"
  },
  {
    "path": "packages/backend/src/passport/github.ts",
    "content": "import { use } from './util'\nimport { Strategy } from 'passport-github2'\n\nuse(\n  'github',\n  (verify) => new Strategy({\n    clientID: process.env.OAUTH_GITHUB_ID,\n    clientSecret: process.env.OAUTH_GITHUB_SECRET,\n    callbackURL: process.env.OAUTH_GITHUB_CALLBACK,\n  }, verify),\n  {\n    scope: ['user:email'],\n  },\n)\n"
  },
  {
    "path": "packages/backend/src/passport/index.ts",
    "content": "import './github'\n"
  },
  {
    "path": "packages/backend/src/passport/util.ts",
    "content": "import { VerifyCallback, Strategy, VerifyFunction } from 'passport-oauth2'\nimport { query as q, values } from 'faunadb'\nimport { useStrategy, deserializeUser } from '@nodepack/plugin-passport'\nimport { hook } from '@nodepack/app-context'\nimport { Context } from '@/context'\nimport passport, { AuthenticateOptions } from 'passport'\nimport fetch from 'node-fetch'\nimport { mapDocument } from '@/util/fauna'\n\nexport interface UserAccount {\n  provider: string\n  profileId: string\n  userRef: values.Ref\n  nickname?: string\n  profileUrl?: string\n  accessToken?: string\n  refreshToken?: string\n}\n\nexport interface OAuthProfile {\n  id: string\n  username?: string\n  displayName?: string\n  email?: string\n  emails?: Array<{ value: string }>\n  photos?: Array<{ value: string }>\n  profileUrl?: string\n}\n\nconst basePath = process.env.BASE_API_PATH || ''\nconst clientBaseUrl = process.env.CLIENT_BASE_URL || ''\n\nexport function use (\n  provider: string,\n  factory: (verify: VerifyFunction) => Strategy,\n  authenticateOptions: AuthenticateOptions,\n) {\n  hook('expressCreate', (ctx: Context) => {\n    const strategy = factory(verifyOAuth(provider, ctx))\n    useStrategy(strategy, (context: Context) => {\n      context.express.get(\n        `${basePath}/auth/${provider}`,\n        passport.authenticate(provider, authenticateOptions),\n      )\n\n      context.express.get(\n        `${basePath}/auth/${provider}/callback`,\n        passport.authenticate(provider, {\n          failureRedirect: `${clientBaseUrl}/login?error=1`,\n        }),\n        (req, res) => {\n          res.redirect(`${clientBaseUrl}/`)\n        },\n      )\n    })\n  })\n}\n\ndeserializeUser(async (passportCtx, { serialized }) => {\n  try {\n    const ctx = passportCtx as Context\n    const doc = await ctx.db.query<values.Document<any>>(\n      q.Get(q.Ref(q.Collection('Users'), serialized)),\n    )\n    return mapDocument(doc)\n  } catch (e) {\n    console.error(e)\n    return null\n  }\n})\n\nexport function verifyOAuth (provider: string, ctx: Context) {\n  return async (accessToken: string, refreshToken: string, profile: OAuthProfile, done: VerifyCallback) => {\n    try {\n      let user: values.Document\n      let account: values.Document<UserAccount> = await ctx.db.query(\n        q.Let(\n          {\n            ref: q.Match(q.Index('useraccounts_by_provider_and_profileid'), provider, profile.id),\n          },\n          q.If(\n            q.Exists(q.Var('ref')),\n            q.Get(q.Var('ref')),\n            null,\n          ),\n        ),\n      )\n      if (!account) {\n        // Create User\n        let email: string\n        if (!profile.email && (!profile.emails || !profile.emails.length)) {\n          // If the user doesn't have any public email\n          // we need to fetch the private ones\n          const data = await fetch(`https://api.github.com/user/emails`, {\n            headers: {\n              authorization: `token ${accessToken}`,\n            },\n          })\n          const json = await data.json()\n          email = json.find((item: any) => item.primary).email\n        } else {\n          email = profile.email || profile.emails[0].value\n        }\n        const avatar = profile.photos.length ? profile.photos[0].value : null\n        const nickname = profile.displayName || profile.username\n\n        user = await ctx.db.query(\n          q.Create(\n            q.Collection('Users'),\n            {\n              data: {\n                email,\n                avatar,\n                nickname,\n              },\n            },\n          ),\n        )\n\n        account = await ctx.db.query(\n          q.Create(\n            q.Collection('UserAccounts'),\n            {\n              data: {\n                provider,\n                profileId: profile.id,\n                userRef: user.ref,\n                nickname,\n                profileUrl: profile.profileUrl,\n                accessToken,\n                refreshToken,\n              } as UserAccount,\n            },\n          ),\n        )\n      } else {\n        user = await ctx.db.query(\n          q.Get(account.data.userRef),\n        )\n      }\n      done(null, user ? mapDocument(user) : null)\n    } catch (err) {\n      console.error(err)\n      done(err)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/backend/src/routes/user.ts",
    "content": "import { ExpressContext } from '@nodepack/plugin-express'\n\nconst basePath = process.env.BASE_API_PATH || ''\nconst clientBaseUrl = process.env.CLIENT_BASE_URL || ''\n\nexport default function ({ express: app }: ExpressContext) {\n  app.use(`${basePath}/auth/logout`, (req, res) => {\n    req.logout()\n    res.redirect(`${clientBaseUrl}/`)\n  })\n}\n"
  },
  {
    "path": "packages/backend/src/schema/admin.ts",
    "content": "import { SchemaDirectiveVisitor } from 'graphql-tools'\nimport { defaultFieldResolver, GraphQLField } from 'graphql'\nimport gql from 'graphql-tag'\nimport { ErrorCode } from '@/const/error-codes'\nimport { query as q } from 'faunadb'\nimport { getIndexObject, indexPackage } from '../util/package-index'\nimport { Resolvers } from '@/generated/schema'\nimport { ApolloError } from '@nodepack/plugin-apollo'\n\nexport const typeDefs = gql`\n\"\"\"\nField requiring authenticated user.\nOtherwise, a 'unauthorized' error is thrown.\n\"\"\"\ndirective @admin on FIELD_DEFINITION\n\nextend type Mutation {\n  indexPackages: Boolean @admin @auth\n  indexPackage (id: ID!): Boolean @admin @auth\n  resetProjectTypeTagCounters: Boolean @admin @auth\n}\n`\n\nexport const schemaDirectives = {\n  admin: class AuthDirective extends SchemaDirectiveVisitor {\n    public visitFieldDefinition (field: GraphQLField<any, any>) {\n      const { resolve = defaultFieldResolver } = field\n      field.resolve = (root, args, context, info) => {\n        if (!context.user.admin) { throw new ApolloError('Access denied', ErrorCode.ERROR_UNAUTHORIZED) }\n        return resolve(root, args, context, info)\n      }\n    }\n  },\n}\n\nexport const resolvers: Resolvers = {\n  Mutation: {\n    // Used to index all packages by admin\n    indexPackages: async (root, args, ctx) => {\n      const { data: allPackages } = await ctx.db.query(\n        q.Map(\n          q.Map(\n            q.Paginate(q.Match(q.Index('all_packages')), { size: 100000 }),\n            q.Lambda('ref', q.Get(q.Var('ref'))),\n          ),\n          q.Lambda('doc',\n            q.Merge(\n              q.Var('doc'),\n              {\n                projectType: q.Get(q.Ref(\n                  q.Collection('ProjectTypes'),\n                  q.Select(['data', 'projectTypeId'], q.Var('doc')),\n                )),\n              },\n            ),\n          ),\n        ),\n      )\n\n      const index = ctx.algolia.initIndex('packages')\n      await index.addObjects(\n        await Promise.all(allPackages.map((doc: any) => getIndexObject(\n          ctx,\n          doc,\n        ))),\n      )\n      return true\n    },\n\n    indexPackage: async (root, { id }, ctx) => {\n      const pkg: any = await ctx.db.query(\n        q.Let({\n          doc: q.Get(q.Ref(\n            q.Collection('Packages'),\n            id,\n          )),\n        },\n        q.Merge(\n          q.Var('doc'),\n          {\n            projectType: q.Get(q.Ref(\n              q.Collection('ProjectTypes'),\n              q.Select(['data', 'projectTypeId'], q.Var('doc')),\n            )),\n          },\n        )),\n      )\n      await indexPackage(ctx, pkg)\n      return true\n    },\n\n    resetProjectTypeTagCounters: async (root, args, ctx) => {\n      const projectTypeMap = new Map<string, Map<string, number>>()\n      const { data: packages } = await ctx.db.query(q.Map(\n        q.Paginate(q.Match(q.Index('all_packages')), { size: 100000 }),\n        q.Lambda('ref', q.Get(q.Var('ref'))),\n      ))\n      for (const pkg of packages) {\n        let counters = projectTypeMap.get(pkg.data.projectTypeId)\n        if (!counters) {\n          counters = new Map<string, number>()\n          projectTypeMap.set(pkg.data.projectTypeId, counters)\n        }\n        for (const tag of pkg.data.info.tags) {\n          let count = counters.get(tag)\n          if (!count) {\n            count = 0\n          }\n          count++\n          counters.set(tag, count)\n        }\n      }\n      await ctx.db.query(\n        q.Do(\n          // Reset counters to empty objects\n          q.Foreach(\n            q.Paginate(q.Match(q.Index('all_projecttypes')), { size: 100000 }),\n            q.Lambda('ref', q.Do(\n              q.Update(q.Var('ref'), { data: { tagMap: null } }),\n              q.Update(q.Var('ref'), { data: { tagMap: {} } }),\n            )),\n          ),\n          // Set counts\n          ...Array.from(projectTypeMap.keys()).map((id) =>\n            q.Update(\n              q.Ref(q.Collection('ProjectTypes'), id),\n              {\n                data: {\n                  tagMap: Array.from(projectTypeMap.get(id).keys()).reduce((map, key) => {\n                    map[key] = projectTypeMap.get(id).get(key)\n                    return map\n                  }, {} as { [key: string]: number }),\n                },\n              },\n            ),\n          ),\n        ),\n      )\n      return true\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/auth.ts",
    "content": "import { SchemaDirectiveVisitor } from 'graphql-tools'\nimport { defaultFieldResolver, GraphQLField } from 'graphql'\nimport gql from 'graphql-tag'\nimport { ApolloError } from '@nodepack/plugin-apollo'\nimport { ErrorCode } from '@/const/error-codes'\n\nexport const typeDefs = gql`\n\"\"\"\nField requiring authenticated user.\nOtherwise, a 'unauthorized' error is thrown.\n\"\"\"\ndirective @auth on FIELD_DEFINITION\n`\n\nexport const schemaDirectives = {\n  auth: class AuthDirective extends SchemaDirectiveVisitor {\n    public visitFieldDefinition (field: GraphQLField<any, any>) {\n      const { resolve = defaultFieldResolver } = field\n      field.resolve = (root, args, context, info) => {\n        if (!context.user) { throw new ApolloError('Not logged in', ErrorCode.ERROR_GUEST) }\n        return resolve(root, args, context, info)\n      }\n    }\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package/bookmark.ts",
    "content": "import gql from 'graphql-tag'\nimport { Resolvers } from '@/generated/schema'\nimport { query as q, values } from 'faunadb'\nimport { mapDocument, mapDocuments } from '@/util/fauna'\n\nexport const typeDefs = gql`\nextend type Package {\n  bookmarked: Boolean\n}\n\nextend type User {\n  bookmarkedPackages: [Package!]!\n}\n\ntype Mutation {\n  togglePackageBookmark (input: TogglePackageBookmarkInput!): Package @auth\n}\n\ninput TogglePackageBookmarkInput {\n  packageId: ID!\n}\n`\n\nexport const resolvers: Resolvers = {\n  Package: {\n    bookmarked: async (pkg, args, ctx) => {\n      if (!ctx.user) { return false }\n      return !!await ctx.db.query(\n        q.Exists(q.Match(\n          q.Index('packagebookmarks_by_package_and_user'),\n          q.Ref(q.Collection('Users'), ctx.user.id),\n          q.Ref(q.Collection('Packages'), pkg.id),\n        )),\n      )\n    },\n  },\n\n  User: {\n    bookmarkedPackages: async (user, args, ctx) => {\n      const { data } = await ctx.db.query(\n        q.Map(\n          q.Paginate(q.Match(q.Index('packagebookmarks_by_userref'), q.Ref(q.Collection('Users'), user.id))),\n          q.Lambda(['ref'], q.Get(q.Select(['data', 'packageRef'], q.Get(q.Var('ref'))))),\n        ),\n      )\n      return mapDocuments(data)\n    },\n  },\n\n  Mutation: {\n    togglePackageBookmark: async (root, { input }, ctx) => {\n      const ref = q.Ref(q.Collection('Packages'), input.packageId)\n      const userRef = q.Ref(q.Collection('Users'), ctx.user.id)\n      const pkg: values.Document<any> = await ctx.db.query(\n        q.Get(ref),\n      )\n      const match = q.Match(\n        q.Index('packagebookmarks_by_package_and_user'),\n        userRef,\n        ref,\n      )\n      if (await ctx.db.query(q.Exists(match))) {\n        await ctx.db.query(\n          q.Delete(q.Select(['ref'], q.Get(match))),\n        )\n      } else {\n        await ctx.db.query(\n          q.Create(\n            q.Collection('PackageBookmarks'),\n            {\n              data: {\n                packageRef: pkg.ref,\n                userRef,\n              },\n            },\n          ),\n        )\n      }\n      return mapDocument(pkg)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package/edit-project-types.ts",
    "content": "import gql from 'graphql-tag'\nimport { Resolvers } from '@/generated/schema'\nimport { query as q, values } from 'faunadb'\nimport { editPackageProjectTypes } from '../package-interface/edit-project-types'\nimport { updatePackageIndex } from '@/util/package-index'\nimport { updateProjectTypeTags } from '@/util/tag-map'\nimport { mapDocument } from '@/util/fauna'\nimport { checkPackageTeamAccess } from '../team/team-access'\n\nexport const typeDefs = gql`\nextend type Mutation {\n  editPackageProjectTypes (input: EditPackageProjectTypesInput!): Package @auth\n}\n`\n\nexport const resolvers: Resolvers = {\n  Mutation: {\n    editPackageProjectTypes: async (root, { input }, ctx) => {\n      const ref = q.Ref(q.Collection('Packages'), input.packageId)\n      await checkPackageTeamAccess(ctx, ref)\n\n      const oldDoc = await ctx.db.query<values.Document<any>>(q.Get(ref))\n\n      const doc = await editPackageProjectTypes(ref, input.projectTypeIds, ctx)\n\n      // Update tags\n      // Dedupe project type refs\n      const projectTypeIds = Array.from(new Set([\n        ...oldDoc.data.projectTypes.map((r: values.Ref) => r.id),\n        ...doc.data.projectTypes.map((r: values.Ref) => r.id),\n      ]))\n      const projectTypeRefs = projectTypeIds.map((id) => q.Ref(q.Collection('ProjectTypes'), id))\n      for (const projectTypeRef of projectTypeRefs) {\n        await updateProjectTypeTags(projectTypeRef, ctx)\n      }\n\n      // Update index\n      await updatePackageIndex(ctx, doc)\n\n      return mapDocument(doc)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package/edit.ts",
    "content": "import gql from 'graphql-tag'\nimport { Resolvers } from '@/generated/schema'\nimport { query as q } from 'faunadb'\nimport { updateProjectTypeTags } from '@/util/tag-map'\nimport { updatePackageIndex } from '@/util/package-index'\nimport { editPackageCommon } from '../package-interface/edit'\nimport { mapDocument } from '@/util/fauna'\nimport { checkPackageTeamAccess } from '../team/team-access'\n\nexport const typeDefs = gql`\nextend type Mutation {\n  editPackageInfo (input: EditPackageInfoInput!): Package @auth\n}\n\ninput EditPackageInfoInput {\n  common: EditPackageInterfaceInput!\n}\n`\n\nexport const resolvers: Resolvers = {\n  Mutation: {\n    editPackageInfo: async (root, { input }, ctx) => {\n      const ref = q.Ref(q.Collection('Packages'), input.common.id)\n      await checkPackageTeamAccess(ctx, ref)\n      const pkg = await editPackageCommon(ref, input.common, ctx)\n\n      // Update tags\n      for (const projectTypeRef of pkg.data.projectTypes) {\n        await updateProjectTypeTags(projectTypeRef, ctx)\n      }\n\n      // Update search index\n      await updatePackageIndex(ctx, pkg)\n\n      return mapDocument(pkg)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package/index.ts",
    "content": "import gql from 'graphql-tag'\nimport { query as q, values, Expr } from 'faunadb'\nimport { Resolvers } from '@/generated/schema'\nimport { mapDocuments, mapDocument } from '@/util/fauna'\n\nexport const typeDefs = gql`\nextend type ProjectType {\n  packages (tags: [String!] = null, after: JSON = null): PackagesPage!\n}\n\ntype PackagesPage {\n  items: [Package!]!\n  after: JSON\n}\n\nextend type Query {\n  package (id: ID!): Package\n  packageByName (name: String!): Package\n}\n`\n\nexport const resolvers: Resolvers = {\n  ProjectType: {\n    packages: async (projectType, input, ctx) => {\n      const projectTypeRef = q.Ref(q.Collection('ProjectTypes'), projectType.id)\n      const { data, after } = await ctx.db.query(\n        q.Map(\n          q.Paginate(\n            input.tags && input.tags.length\n              ? q.Join(\n                q.Intersection(\n                  q.Match(q.Index('packages_by_project_type'), projectTypeRef),\n                  q.Union(\n                    ...input.tags.map((tag: string) => q.Match(q.Index('packages_by_tag'), tag)),\n                  ),\n                ),\n                q.Index('packages_by_ref_sort_by_stars_desc'),\n              )\n              : q.Match(q.Index('packages_by_project_type_sort_by_stars_desc'), projectTypeRef),\n            { size: 12, after: input.after ? new Expr(input.after) : null },\n          ),\n          q.Lambda(['stars', 'ref'], q.Get(q.Var('ref'))),\n        ),\n      )\n      return {\n        items: mapDocuments(data),\n        after,\n      }\n    },\n  },\n\n  Query: {\n    package: async (root, { id }, ctx) => {\n      const doc = await ctx.db.query<values.Document<any>>(\n        q.Get(q.Ref(q.Collection('Packages'), id)),\n      )\n      if (doc.data) {\n        return mapDocument(doc)\n      }\n    },\n\n    packageByName: async (root, { name } , ctx) => {\n      try {\n        const doc = await ctx.db.query<values.Document<any>>(\n          q.Get(q.Match(q.Index('packages_by_name'), name)),\n        )\n        return mapDocument(doc)\n      } catch (e) {\n        // Nothing\n      }\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-interface/data-source.ts",
    "content": "import gql from 'graphql-tag'\nimport { Resolvers } from '@/generated/schema'\n\nexport const typeDefs = gql`\nextend interface PackageInterface {\n  dataSources: [PackageDataSource!]!\n}\n\nextend type Package {\n  dataSources: [PackageDataSource!]!\n}\n\nextend type PackageProposal {\n  dataSources: [PackageDataSource!]!\n}\n\ntype PackageDataSource {\n  type: String!\n  data: JSON\n}\n\ninput DataSourcesInput {\n  github: GithubDataSourceInput\n  npm: NpmDataSourceInput\n}\n\ninput GithubDataSourceInput {\n  owner: String!\n  repo: String!\n}\n\ninput NpmDataSourceInput {\n  name: String!\n}\n`\n\nexport interface NpmDataSource {\n  name: string\n}\n\nexport interface GithubDataSource {\n  owner: string\n  repo: string\n}\n\nexport interface PackageDataSources {\n  npm: NpmDataSource\n  github: GithubDataSource\n}\n\nexport const resolvers: Resolvers = {\n  PackageInterface: {\n    dataSources: async (pkg) => {\n      return Object.keys(pkg.dataSources || {}).map((key) => ({\n        type: key,\n        // @ts-ignore\n        data: pkg.dataSources[key],\n      }))\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-interface/db-types.ts",
    "content": "import { PackageInterface } from '@/generated/schema'\nimport { PackageDataSources, GithubDataSource } from './data-source'\nimport { values } from 'faunadb'\n\nexport interface PackageMetadata<T> {\n  version: number\n  ts: number\n  data: T\n}\n\nexport interface PackageMetadatas {\n  npm?: PackageMetadata<any>\n  github?: PackageMetadata<any>\n}\n\nexport interface DBPackageInterface extends Omit<PackageInterface, 'dataSources'> {\n  ref: values.Ref\n  dataSources: PackageDataSources\n  metadata: PackageMetadatas\n\n  /**\n   * @deprecated\n   */\n  github: GithubDataSource\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-interface/edit-project-types.ts",
    "content": "import gql from 'graphql-tag'\nimport { Context } from '@/context'\nimport { Expr, query as q, values } from 'faunadb'\nimport { ApolloError } from '@nodepack/plugin-apollo'\nimport { ErrorCode } from '@/const/error-codes'\n\nexport const typeDefs = gql`\ninput EditPackageProjectTypesInput {\n  packageId: ID!\n  projectTypeIds: [ID!]!\n}\n`\n\nexport async function editPackageProjectTypes (\n  ref: Expr,\n  projectTypeIds: string[],\n  ctx: Context,\n) {\n  if (!projectTypeIds.length) {\n    throw new ApolloError('Select at least one project type', ErrorCode.ERROR_VALIDATION)\n  }\n\n  // Update data\n  const item = await ctx.db.query<values.Document<any>>(\n    q.Update(ref, {\n      data: {\n        projectTypes: projectTypeIds.map((id) => q.Ref(q.Collection('ProjectTypes'), id)),\n      },\n    }),\n  )\n  return item\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-interface/edit.ts",
    "content": "import gql from 'graphql-tag'\nimport { Context } from '@/context'\nimport { sanitizeTags } from '@/util/tags'\nimport { query as q, Expr, values } from 'faunadb'\nimport { EditPackageInterfaceInput } from '@/generated/schema'\n\nexport const typeDefs = gql`\ninput EditPackageInterfaceInput {\n  id: ID!\n  info: PackageInfoInput\n  dataSources: DataSourcesInput\n}\n`\n\nexport async function editPackageCommon (\n  ref: Expr,\n  input: EditPackageInterfaceInput,\n  ctx: Context,\n) {\n  // Process tags\n  input.info.tags = sanitizeTags(input.info.tags)\n\n  // Update data\n  const item = await ctx.db.query<values.Document<any>>(\n    q.Update(ref, {\n      data: {\n        ...input.info ? { info: input.info } : {},\n        ...input.dataSources ? {\n          dataSources: input.dataSources,\n          metadata: {\n            npm: null,\n            github: null,\n          },\n        } : {},\n      },\n    }),\n  )\n  return item\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-interface/index.ts",
    "content": "import gql from 'graphql-tag'\nimport { Resolvers } from '@/generated/schema'\nimport { query as q } from 'faunadb'\nimport { DBProjectType } from '../project-type/db-types'\nimport { mapDocuments } from '@/util/fauna'\n\nexport const typeDefs = gql`\ninterface PackageInterface {\n  id: ID!\n  name: String!\n  projectTypes: [ProjectType!]!\n  info: PackageInfo!\n}\n\ntype Package implements PackageInterface {\n  id: ID!\n  name: String!\n  projectTypes: [ProjectType!]!\n  info: PackageInfo!\n}\n\ntype PackageProposal implements PackageInterface {\n  id: ID!\n  name: String!\n  projectTypes: [ProjectType!]!\n  info: PackageInfo!\n}\n\ntype PackageMaintainer {\n  name: String\n  email: String\n  avatar: String\n}\n\ntype PackageInfo {\n  tags: [String!]!\n}\n\ninput PackageInfoInput {\n  tags: [String!]!\n}\n`\n\nexport const resolvers: Resolvers = {\n  PackageInterface: {\n    __resolveType: (pkg: any) => {\n      if (pkg.ref.collection.id === 'Packages') { return 'Package' }\n      if (pkg.ref.collection.id === 'PackageProposals') { return 'PackageProposal' }\n      return null\n    },\n\n    projectTypes: async (pkg, input, ctx) => {\n      const list = await ctx.db.query<any[]>(\n        q.Map(\n          pkg.projectTypes,\n          q.Lambda(['ref'], q.Get(q.Var('ref'))),\n        ),\n      )\n      return mapDocuments(list) as DBProjectType[]\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-interface/insight.ts",
    "content": "import gql from 'graphql-tag'\nimport { Resolvers } from '@/generated/schema'\n\nexport const typeDefs = gql`\nextend interface PackageInterface {\n  insight: PackageInsight!\n}\n\nextend type Package {\n  insight: PackageInsight!\n}\n\nextend type PackageProposal {\n  insight: PackageInsight!\n}\n\ntype PackageInsight {\n  npm: PackageNpmInsight\n}\n\ntype PackageNpmInsight {\n  downloads (range: PackageNpmInsightDownloadsRange!): Int!\n  downloadsPoints (range: PackageNpmInsightDownloadsRange!): [DownloadsPoint!]!\n}\n\ntype DownloadsPoint {\n  downloads: Int!\n  day: Date!\n}\n\nenum PackageNpmInsightDownloadsRange {\n  day\n  week\n  month\n}\n`\n\nexport const resolvers: Resolvers = {\n  PackageInterface: {\n    insight: (pkg) => pkg as any,\n  },\n\n  PackageInsight: {\n    npm: (pkg: any) => pkg.dataSources.npm ? pkg : null,\n  },\n\n  PackageNpmInsight: {\n    downloads: async (pkg: any, { range }, ctx) => {\n      const data = await ctx.npmApi(`/downloads/point/last-${range}/${encodeURIComponent(pkg.dataSources.npm.name)}`)\n      return data.downloads\n    },\n\n    downloadsPoints: async (pkg: any, { range }, ctx) => {\n      const data = await ctx.npmApi(`/downloads/range/last-${range}/${encodeURIComponent(pkg.dataSources.npm.name)}`)\n      return data.downloads\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-interface/package-metadata.ts",
    "content": "import { Resolvers } from '@/generated/schema'\nimport { getNpmMetadata, getGithubMetadata } from '@/util/metadata'\nimport { getReadme } from '@/util/readme'\nimport gql from 'graphql-tag'\n\nexport const typeDefs = gql`\nextend interface PackageInterface {\n  stars: Int\n  repo: String\n  defaultLogo: String\n  maintainers: [PackageMaintainer!]!\n  homepage: String\n  license: String\n  description: String\n  readme: String\n}\n\nextend type Package {\n  stars: Int\n  repo: String\n  defaultLogo: String\n  maintainers: [PackageMaintainer!]!\n  homepage: String\n  license: String\n  description: String\n  readme: String\n}\n\nextend type PackageProposal {\n  stars: Int\n  repo: String\n  defaultLogo: String\n  maintainers: [PackageMaintainer!]!\n  homepage: String\n  license: String\n  description: String\n  readme: String\n}\n`\n\nexport const resolvers: Resolvers = {\n  PackageInterface: {\n    stars: async (pkg, args, ctx) => (await getGithubMetadata(pkg, ctx)).stars,\n    repo: async (pkg, args, ctx) => (await getGithubMetadata(pkg, ctx)).htmlUrl,\n    defaultLogo: async (pkg, args, ctx) => {\n      if (pkg.dataSources.npm) {\n        const data = await getNpmMetadata(pkg, ctx)\n        if (data?.logo) {\n          return data.logo\n        } else if (data?.awesomejs?.logo) {\n          return data.awesomejs.logo\n        }\n      }\n      return (await getGithubMetadata(pkg, ctx)).owner?.avatar\n    },\n    maintainers: async (pkg, args, ctx) => (await getNpmMetadata(pkg, ctx)).maintainers || [],\n    homepage: async (pkg, args, ctx) => (await getNpmMetadata(pkg, ctx)).homepage,\n    license: async (pkg, args, ctx) => (await getNpmMetadata(pkg, ctx)).license,\n    description: async (pkg, args, ctx) => (await getGithubMetadata(pkg, ctx)).description ||\n      (await getNpmMetadata(pkg, ctx)).description,\n    readme: async (pkg, args, ctx) => getReadme(pkg, ctx),\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-interface/releases.ts",
    "content": "import { getGithubDataSource } from '@/util/metadata'\nimport gql from 'graphql-tag'\nimport { Resolvers } from '@/generated/schema'\nimport { getPageTotalCount } from '@/util/github'\n\nexport const typeDefs = gql`\ntype PackageRelease {\n  id: ID!\n  date: Date\n  title: String\n  tagName: String\n  description: String\n  prerelease: Boolean\n  assets: [PackageReleaseAsset!]!\n}\n\ntype PackageReleaseAsset {\n  name: String!\n  downloadUrl: String!\n  size: Int!\n}\n\nextend interface PackageInterface {\n  releases: [PackageRelease!]!\n  releaseCount: Int\n  tagCount: Int\n}\n\nextend type Package {\n  releases: [PackageRelease!]!\n  releaseCount: Int\n  tagCount: Int\n}\n\nextend type PackageProposal {\n  releases: [PackageRelease!]!\n  releaseCount: Int\n  tagCount: Int\n}\n`\n\nexport const resolvers: Resolvers = {\n  PackageInterface: {\n    releases: async (pkg, args, ctx) => {\n      const { owner, repo } = await getGithubDataSource(pkg, ctx)\n      if (repo) {\n        const { data } = await ctx.github.repos.listReleases({\n          owner,\n          repo,\n          headers: {\n            accept: 'application/vnd.github.3.html',\n          },\n        })\n        return data.filter((i) => !i.draft).map((item) => ({\n          id: item.id.toString(),\n          date: item.published_at,\n          title: item.name || item.tag_name,\n          tagName: item.tag_name,\n          // @ts-ignore\n          description: item.body_html,\n          prerelease: item.prerelease,\n          assets: item.assets.map((asset) => ({\n            name: asset.name,\n            downloadUrl: asset.browser_download_url,\n            size: asset.size,\n          })),\n        }))\n      }\n\n      return []\n    },\n\n    releaseCount: async (pkg, args, ctx) => {\n      const { owner, repo } = await getGithubDataSource(pkg, ctx)\n      if (repo) {\n        const result = await ctx.github.repos.listReleases({\n          owner,\n          repo,\n          per_page: 1,\n        })\n        return getPageTotalCount(result)\n      }\n    },\n\n    tagCount: async (pkg, args, ctx) => {\n      const { owner, repo } = await getGithubDataSource(pkg, ctx)\n      if (repo) {\n        const result = await ctx.github.repos.listTags({\n          owner,\n          repo,\n          per_page: 1,\n        })\n        return getPageTotalCount(result)\n      }\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-proposal/approve.ts",
    "content": "import gql from 'graphql-tag'\nimport { query as q, Expr, values } from 'faunadb'\nimport { indexPackage } from '@/util/package-index'\nimport { Resolvers } from '@/generated/schema'\nimport { mapDocument } from '@/util/fauna'\nimport { checkPackageTeamAccess } from '../team/team-access'\n\nexport const typeDefs = gql`\nextend type Mutation {\n  approvePackageProposal (input: ApprovePackageProposalInput!): Package @auth\n}\n\ninput ApprovePackageProposalInput {\n  proposalId: ID!\n}\n`\n\nexport const resolvers: Resolvers = {\n  Mutation: {\n    approvePackageProposal: async (root, { input }, ctx) => {\n      const ref = q.Ref(q.Collection('PackageProposals'), input.proposalId)\n      await checkPackageTeamAccess(ctx, ref)\n\n      const pkgProposal: any = await ctx.db.query(\n        q.Get(ref),\n      )\n\n      // Update tag maps\n      const projectTypes = await ctx.db.query<Array<values.Document<any>>>(q.Map(\n        pkgProposal.data.projectTypes,\n        q.Lambda(['ref'], q.Get(q.Var('ref'))),\n      ))\n      const projectTypeUpdates: Expr[] = []\n      for (const projectType of projectTypes) {\n        const tagMap = projectType.data.tagMap\n        for (const tag of pkgProposal.data.info.tags) {\n          tagMap[tag] = tagMap[tag] || 0\n          tagMap[tag]++\n        }\n        projectTypeUpdates.push(q.Update(\n          projectType.ref,\n          {\n            data: {\n              tagMap,\n            },\n          },\n        ))\n      }\n\n      const pkg: any = await ctx.db.query(\n        q.Do(\n          q.Delete(pkgProposal.ref),\n          ...projectTypeUpdates,\n          q.Create(\n            q.Collection('Packages'),\n            {\n              data: {\n                name: pkgProposal.data.name,\n                projectTypes: projectTypes.map((pt) => pt.ref),\n                info: pkgProposal.data.info,\n                dataSources: pkgProposal.data.dataSources,\n                metadata: pkgProposal.data.metadata,\n              },\n            },\n          ),\n        ),\n      )\n\n      await indexPackage(ctx, pkg)\n\n      return mapDocument(pkg)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-proposal/db-types.ts",
    "content": "import { values } from 'faunadb'\nimport { PackageProposal } from '@/generated/schema'\n\nexport interface DBPackageProposal extends PackageProposal {\n  projectTypeRef: values.Ref\n  userRef: values.Ref\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-proposal/edit-project-types.ts",
    "content": "import gql from 'graphql-tag'\nimport { Resolvers } from '@/generated/schema'\nimport { query as q } from 'faunadb'\nimport { editPackageProjectTypes } from '../package-interface/edit-project-types'\nimport { mapDocument } from '@/util/fauna'\nimport { checkPackageTeamAccess } from '../team/team-access'\n\nexport const typeDefs = gql`\nextend type Mutation {\n  editPackageProposalProjectTypes (input: EditPackageProjectTypesInput!): PackageProposal @auth\n}\n`\n\nexport const resolvers: Resolvers = {\n  Mutation: {\n    editPackageProposalProjectTypes: async (root, { input }, ctx) => {\n      const ref = q.Ref(q.Collection('PackageProposals'), input.packageId)\n      await checkPackageTeamAccess(ctx, ref)\n      const doc = await editPackageProjectTypes(ref, input.projectTypeIds, ctx)\n      return mapDocument(doc)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-proposal/edit.ts",
    "content": "import gql from 'graphql-tag'\nimport { query as q } from 'faunadb'\nimport { Resolvers } from '@/generated/schema'\nimport { editPackageCommon } from '../package-interface/edit'\nimport { mapDocument } from '@/util/fauna'\nimport { checkPackageTeamAccess } from '../team/team-access'\n\nexport const typeDefs = gql`\nextend type Mutation {\n  editPackageProposalInfo (input: EditPackageProposalInfoInput!): PackageProposal @auth\n}\n\ninput EditPackageProposalInfoInput {\n  common: EditPackageInterfaceInput!\n}\n`\n\nexport const resolvers: Resolvers = {\n  Mutation: {\n    editPackageProposalInfo: async (root, { input }, ctx) => {\n      const ref = q.Ref(q.Collection('PackageProposals'), input.common.id)\n      await checkPackageTeamAccess(ctx, ref)\n      const item = await editPackageCommon(ref, input.common, ctx)\n      return mapDocument(item)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-proposal/index.ts",
    "content": "import gql from 'graphql-tag'\nimport { query as q, values } from 'faunadb'\nimport { Resolvers } from '@/generated/schema'\nimport { mapDocument, mapDocuments } from '@/util/fauna'\n\nexport const typeDefs = gql`\nextend type PackageProposal {\n  user: User\n}\n\nextend type ProjectType {\n  packageProposals: [PackageProposal!]!\n  packageProposalCount: Int!\n}\n\nextend type Query {\n  packageProposal (id: ID!): PackageProposal\n  packageProposalByName (name: String!): PackageProposal\n}\n`\nexport const resolvers: Resolvers = {\n  PackageProposal: {\n    user: async (proposal, args, ctx) => {\n      try {\n        const user = await ctx.db.query<values.Document<any>>(\n          q.Get(proposal.userRef),\n        )\n        return mapDocument(user)\n      } catch (e) {\n        // Nothing\n      }\n    },\n  },\n\n  ProjectType: {\n    packageProposals: async (projectType, args, ctx) => {\n      const { data } = await ctx.db.query(\n        q.Map(\n          q.Paginate(\n            q.Match(\n              q.Index('packageproposals_by_projecttypes_sort_by_upvote'),\n              q.Ref(q.Collection('ProjectTypes'), projectType.id),\n            ),\n          ),\n          q.Lambda(['upvotes', 'ref'], q.Get(q.Var('ref'))),\n        ),\n      )\n      return mapDocuments(data)\n    },\n\n    packageProposalCount: async (projectType, args, ctx) => {\n      const { data } = await ctx.db.query(\n        q.Count(\n          q.Paginate(\n            q.Match(\n              q.Index('packageproposals_by_projecttypes_sort_by_upvote'),\n              q.Ref(q.Collection('ProjectTypes'), projectType.id),\n            ),\n          ),\n        ),\n      )\n      return data[0]\n    },\n  },\n\n  Query: {\n    packageProposal: async (root, { id }, ctx) => {\n      const doc = await ctx.db.query<values.Document<any>>(\n        q.Get(q.Ref(q.Collection('PackageProposals'), id)),\n      )\n      return mapDocument(doc)\n    },\n\n    packageProposalByName: async (root, { name }, ctx) => {\n      try {\n        const doc = await ctx.db.query<values.Document<any>>(\n          q.Get(q.Match(q.Index('packageproposal_by_name'), name)),\n        )\n        return mapDocument(doc)\n      } catch (e) {\n        // Nothing\n      }\n    },\n  },\n}\n\n"
  },
  {
    "path": "packages/backend/src/schema/package-proposal/propose.ts",
    "content": "import gql from 'graphql-tag'\nimport { query as q, values } from 'faunadb'\nimport { sanitizeTags } from '@/util/tags'\nimport { ApolloError } from '@nodepack/plugin-apollo'\nimport { Resolvers } from '@/generated/schema'\nimport { mapDocument } from '@/util/fauna'\n\nexport const typeDefs = gql`\nextend type Mutation {\n  proposePackage (input: ProposePackageInput!): PackageProposal @auth\n}\n\ninput ProposePackageInput {\n  projectTypeId: ID!\n  packageName: String!\n  tags: [String!]!\n}\n`\n\nexport const resolvers: Resolvers = {\n  Mutation: {\n    proposePackage: async (root, { input }, ctx) => {\n      input.tags = sanitizeTags(input.tags)\n\n      if (await ctx.db.query(\n        q.Exists(q.Match(q.Index('packageproposal_by_name'), input.packageName)),\n      )) {\n        throw new ApolloError('Package proposal already exists')\n      }\n\n      if (await ctx.db.query(\n        q.Exists(q.Match(q.Index('packages_by_name'), input.packageName)),\n      )) {\n        throw new ApolloError('Package was already added')\n      }\n\n      // Npm check\n      try {\n        await ctx.npm(`/${encodeURIComponent(input.packageName)}`)\n      } catch (e) {\n        throw new ApolloError(`Package not found on npm`)\n      }\n\n      const doc = await ctx.db.query<values.Document<any>>(\n        q.Create(\n          q.Collection('PackageProposals'),\n          {\n            data: {\n              name: input.packageName,\n              projectTypes: [\n                q.Ref(q.Collection('ProjectTypes'), input.projectTypeId),\n              ],\n              userRef: q.Ref(q.Collection('Users'), ctx.user.id),\n              upvotes: 0,\n              info: {\n                tags: input.tags,\n              },\n              dataSources: {\n                npm: {\n                  name: input.packageName,\n                },\n              },\n            },\n          },\n        ),\n      )\n      return mapDocument(doc)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-proposal/remove.ts",
    "content": "import gql from 'graphql-tag'\nimport { Resolvers } from '@/generated/schema'\nimport { query as q } from 'faunadb'\nimport { checkPackageTeamAccess } from '../team/team-access'\n\nexport const typeDefs = gql`\ninput RemovePackageProposalInput {\n  id: ID!\n}\n\nextend type Mutation {\n  removePackageProposal (input: RemovePackageProposalInput!): Boolean!\n}\n`\n\nexport const resolvers: Resolvers = {\n  Mutation: {\n    removePackageProposal: async (root, { input }, ctx) => {\n      const ref = q.Ref(q.Collection('PackageProposals'), input.id)\n      await checkPackageTeamAccess(ctx, ref)\n      await ctx.db.query(q.Delete(ref))\n      return true\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/package-proposal/upvote.ts",
    "content": "import gql from 'graphql-tag'\nimport { query as q, values } from 'faunadb'\nimport { Resolvers } from '@/generated/schema'\nimport { mapDocument } from '@/util/fauna'\n\nexport const typeDefs = gql`\nextend type PackageProposal {\n  upvotes: Int!\n  upvoted: Boolean!\n}\n\nextend type Mutation {\n  togglePackageProposalUpvote (input: TogglePackageProposalUpvoteInput!): PackageProposal @auth\n}\n\ninput TogglePackageProposalUpvoteInput {\n  proposalId: ID!\n}\n`\n\nexport const resolvers: Resolvers = {\n  PackageProposal: {\n    upvoted: async (pkg, args, ctx) => {\n      if (!ctx.user) { return false }\n      return !!await ctx.db.query(\n        q.Exists(q.Match(\n          q.Index('packageproposalupvotes_by_proposal_and_user'),\n          q.Ref(q.Collection('Users'), ctx.user.id),\n          q.Ref(q.Collection('PackageProposals'), pkg.id),\n        )),\n      )\n    },\n  },\n\n  Mutation: {\n    togglePackageProposalUpvote: async (root, { input }, ctx) => {\n      const ref = q.Ref(q.Collection('PackageProposals'), input.proposalId)\n      const userRef = q.Ref(q.Collection('Users'), ctx.user.id)\n      const match = q.Match(\n        q.Index('packageproposalupvotes_by_proposal_and_user'),\n        userRef,\n        ref,\n      )\n      const doc = await ctx.db.query<values.Document<any>>(\n        q.Get(ref),\n      )\n      if (await ctx.db.query(q.Exists(match))) {\n        await ctx.db.query(\n          q.Do(\n            q.Delete(q.Select(['ref'], q.Get(match))),\n            q.Update(ref, {\n              data: {\n                upvotes: --doc.data.upvotes,\n              },\n            }),\n          ),\n        )\n      } else {\n        await ctx.db.query(\n          q.Do(\n            q.Create(\n              q.Collection('PackageProposalUpvotes'),\n              {\n                data: {\n                  proposalRef: ref,\n                  userRef,\n                },\n              },\n            ),\n            q.Update(ref, {\n              data: {\n                upvotes: ++doc.data.upvotes,\n              },\n            }),\n          ),\n        )\n      }\n      return mapDocument(doc)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/project-type/bookmarks.ts",
    "content": "import gql from 'graphql-tag'\nimport { query as q, values } from 'faunadb'\nimport { Resolvers } from '@/generated/schema'\nimport { mapDocument } from '@/util/fauna'\n\nexport const typeDefs = gql`\nextend type ProjectType {\n  bookmarked: Boolean\n}\n\nextend type Mutation {\n  toggleProjectTypeBookmark (input: ToggleProjectTypeBookmarkInput!): ProjectType @auth\n}\n\ninput ToggleProjectTypeBookmarkInput {\n  projectTypeId: ID!\n}\n`\n\nexport const resolvers: Resolvers = {\n  ProjectType: {\n    bookmarked: (projectType, args, ctx) => {\n      return ctx.user && ctx.user.projectTypeBookmarks &&\n        ctx.user.projectTypeBookmarks.includes(projectType.id)\n    },\n  },\n\n  Mutation: {\n    toggleProjectTypeBookmark: async (root, { input: { projectTypeId } }, ctx) => {\n      let bookmarks = ctx.user.projectTypeBookmarks\n      if (!bookmarks) {\n        bookmarks = [projectTypeId]\n      } else {\n        const index = bookmarks.indexOf(projectTypeId)\n        if (index === -1) {\n          bookmarks.push(projectTypeId)\n        } else {\n          bookmarks.splice(index, 1)\n        }\n      }\n      const doc = await ctx.db.query<values.Document<any>>(\n        q.Do(\n          q.Update(\n            q.Ref(q.Collection('Users'), ctx.user.id),\n            {\n              data: {\n                projectTypeBookmarks: bookmarks,\n              },\n            },\n          ),\n          q.Get(q.Ref(q.Collection('ProjectTypes'), projectTypeId)),\n        ),\n      )\n      if (doc.data) {\n        return mapDocument(doc)\n      }\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/project-type/db-types.ts",
    "content": "import { ProjectType } from '@/generated/schema'\n\nexport interface DBProjectType extends ProjectType {\n  tagMap: { [key: string]: number }\n}\n"
  },
  {
    "path": "packages/backend/src/schema/project-type/index.ts",
    "content": "import gql from 'graphql-tag'\nimport { query as q, values } from 'faunadb'\nimport { Resolvers } from '@/generated/schema'\nimport { isSpecialTag } from '@awesomejs/shared-utils/tags'\nimport { DBProjectType } from './db-types'\nimport { mapDocument, mapDocuments } from '@/util/fauna'\nimport { hasTeamAccess } from '../team/team-access'\n\nexport const typeDefs = gql`\ntype ProjectType {\n  id: ID!\n  name: String!\n  slug: String!\n  logo: String!\n  popularTags: [Tag!]!\n  tags: [Tag!]!\n  inTeam: Boolean!\n}\n\ntype Tag {\n  id: ID!\n  count: Int!\n}\n\nextend type Query {\n  projectTypes: [ProjectType!]!\n  projectType (id: ID!): ProjectType\n  projectTypeBySlug (slug: String!): ProjectType\n}\n`\n\nfunction getSortedTags (projectType: DBProjectType) {\n  return Object.keys(projectType.tagMap).filter(\n    (key) => key !== projectType.name.toLowerCase(),\n  ).map((key) => ({\n    id: key,\n    count: projectType.tagMap[key],\n  })).sort(\n    (a, b) => {\n      if (isSpecialTag(a.id)) { return -1 }\n      if (isSpecialTag(b.id)) { return 1 }\n      return b.count - a.count\n    },\n  )\n}\n\nexport const resolvers: Resolvers = {\n  ProjectType: {\n    popularTags: (projectType) => {\n      return getSortedTags(projectType).slice(0, 8)\n    },\n\n    tags: (projectType) => {\n      return getSortedTags(projectType)\n    },\n\n    inTeam: (projectType, args, ctx) => hasTeamAccess(ctx, projectType.id),\n  },\n\n  Query: {\n    projectTypes: async (root, args, ctx) => {\n      const { data } = await ctx.db.query(\n        q.Map(\n          q.Paginate(\n            q.Match(q.Index('projecttypes_sort_by_name_asc')),\n          ),\n          q.Lambda(['name', 'ref'], q.Get(q.Var('ref'))),\n        ),\n      )\n      return mapDocuments(data)\n    },\n\n    projectType: async (root, { id }, ctx) => {\n      const doc = await ctx.db.query<values.Document<any>>(\n        q.Get(q.Ref(q.Collection('ProjectTypes'), id)),\n      )\n      return mapDocument(doc)\n    },\n\n    projectTypeBySlug: async (root, { slug }, ctx) => {\n      const doc = await ctx.db.query<values.Document<any>>(\n        q.Get(q.Match(q.Index('projecttypes_by_slug'), slug)),\n      )\n      return mapDocument(doc)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/schema/scalar/data.ts",
    "content": "import gql from 'graphql-tag'\nimport { GraphQLScalarType, Kind } from 'graphql'\n\nexport const typeDefs = gql`\nscalar Date\n`\n\nexport const resolvers = {\n  Date: new GraphQLScalarType({\n    name: 'Date',\n    description: 'Date timestamp. It\\'s serialized as a time number in ms, for example `1550923964562`.',\n    parseValue (value) {\n      return new Date(value)\n    },\n    serialize (value: Date) {\n      return new Date(value).getTime()\n    },\n    parseLiteral (ast) {\n      if (ast.kind === Kind.INT) {\n        return new Date(ast.value)\n      }\n      return null\n    },\n  }),\n}\n"
  },
  {
    "path": "packages/backend/src/schema/scalar/json.ts",
    "content": "import gql from 'graphql-tag'\nimport GraphQLJSON from 'graphql-type-json'\n\nexport const typeDefs = gql`\nscalar JSON\n`\n\nexport const resolvers = {\n  JSON: GraphQLJSON,\n}\n"
  },
  {
    "path": "packages/backend/src/schema/team/index.ts",
    "content": "import gql from 'graphql-tag'\nimport { Resolvers, CreateTeamInput, EditTeamInput } from '@/generated/schema'\nimport { query as q, values } from 'faunadb'\nimport { mapDocuments, mapDocument } from '@/util/fauna'\nimport { ApolloError } from '@nodepack/plugin-apollo'\nimport { ErrorCode } from '@/const/error-codes'\n\nexport const typeDefs = gql`\ntype Team {\n  id: ID!\n  name: String!\n  projectTypes: [ProjectType!]!\n  users: [User!]!\n}\n\nextend type Query {\n  team (id: ID!): Team\n  allTeams: [Team!]! @admin @auth\n}\n\ninput CreateTeamInput {\n  name: String!\n  projectTypeIds: [ID!]!\n  userIds: [ID!]!\n}\n\ninput EditTeamInput {\n  id: ID!\n  name: String!\n  projectTypeIds: [ID!]!\n  userIds: [ID!]!\n}\n\nextend type Mutation {\n  createTeam (input: CreateTeamInput!): Team @admin @auth\n  editTeam (input: EditTeamInput!): Team @admin @auth\n}\n`\n\nfunction validateInput (\n  input: CreateTeamInput | EditTeamInput,\n) {\n  if (!input.name) {\n    throw new ApolloError('Team name is required', ErrorCode.ERROR_VALIDATION)\n  }\n\n  if (!input.projectTypeIds.length) {\n    throw new ApolloError('Select at least one project type', ErrorCode.ERROR_VALIDATION)\n  }\n}\n\nexport const resolvers: Resolvers = {\n  Team: {\n    projectTypes: async (team, args, ctx) => {\n      return mapDocuments(await ctx.db.query<Array<values.Document<any>>>(q.Map(\n        team.projectTypes,\n        q.Lambda(['ref'], q.Get(q.Var('ref'))),\n      )))\n    },\n\n    users: async (team, args, ctx) => {\n      return mapDocuments(await ctx.db.query<Array<values.Document<any>>>(q.Map(\n        team.users,\n        q.Lambda(['ref'], q.Get(q.Var('ref'))),\n      )))\n    },\n  },\n\n  Query: {\n    team: async (root, { id }, ctx) => {\n      const doc = await ctx.db.query<values.Document<any>>(q.Get(q.Ref(q.Collection('teams'), id)))\n      return mapDocument(doc)\n    },\n\n    allTeams: async (root, args, ctx) => {\n      const { data } = await ctx.db.query(q.Map(\n        q.Paginate(q.Match(q.Index('all_teams')), { size: 1000 }),\n        q.Lambda(['ref'], q.Get(q.Var('ref'))),\n      ))\n      return mapDocuments(data)\n    },\n  },\n\n  Mutation: {\n    createTeam: async (root, { input }, ctx) => {\n      validateInput(input)\n      const doc = await ctx.db.query<values.Document<any>>(q.Create(q.Collection('teams'), {\n        data: {\n          name: input.name,\n          projectTypes: input.projectTypeIds.map((id) => q.Ref(q.Collection('ProjectTypes'), id)),\n          users: input.userIds.map((id) => q.Ref(q.Collection('Users'), id)),\n        },\n      }))\n      return mapDocument(doc)\n    },\n\n    editTeam: async (root, { input }, ctx) => {\n      validateInput(input)\n      const doc = await ctx.db.query<values.Document<any>>(q.Update(q.Ref(q.Collection('teams'), input.id), {\n        data: {\n          name: input.name,\n          projectTypes: input.projectTypeIds.map((id) => q.Ref(q.Collection('ProjectTypes'), id)),\n          users: input.userIds.map((id) => q.Ref(q.Collection('Users'), id)),\n        },\n      }))\n      return mapDocument(doc)\n    },\n  },\n}\n\n"
  },
  {
    "path": "packages/backend/src/schema/team/team-access.ts",
    "content": "import { User, Context } from '@/context'\nimport { query as q, values } from 'faunadb'\nimport { ApolloError } from '@nodepack/plugin-apollo'\nimport { ErrorCode } from '@/const/error-codes'\n\nexport async function loadTeams (\n  user: User,\n  ctx: Context,\n) {\n  if (!user.teamsProjectTypes) {\n    const { data } = await ctx.db.query(q.Map(\n      q.Paginate(q.Match(q.Index('teams_by_user'), user.ref), { size: 1000 }),\n      q.Lambda(['ref'], q.Select(['data', 'projectTypes'], q.Get(q.Var('ref')))),\n    ))\n    const teamsProjectTypes = []\n    for (const projectTypes of data) {\n      teamsProjectTypes.push(...projectTypes)\n    }\n    user.teamsProjectTypes = teamsProjectTypes\n  }\n}\n\nexport async function hasTeamAccess (\n  ctx: Context,\n  projectTypeId: string,\n  user: User = ctx.user,\n): Promise<boolean> {\n  if (!ctx.user) { return false }\n  if (ctx.user.admin) { return true }\n\n  await loadTeams(user, ctx)\n\n  return user.teamsProjectTypes.some((ref) => ref.id === projectTypeId)\n}\n\nexport async function checkTeamAccess (\n  ctx: Context,\n  projectTypeId: string,\n  user: User = ctx.user,\n) {\n  if (!await hasTeamAccess(ctx, projectTypeId, user)) {\n    throw new ApolloError('Unauthorized', ErrorCode.ERROR_UNAUTHORIZED)\n  }\n}\n\nexport async function hasPackageTeamAccess (\n  ctx: Context,\n  pkgRef: any,\n  user: User = ctx.user,\n) {\n  if (!ctx.user) { return false }\n  if (ctx.user.admin) { return true }\n\n  const doc = await ctx.db.query<values.Document<any>>(q.Get(pkgRef))\n  for (const ptRef of doc.data.projectTypes) {\n    if (await hasTeamAccess(ctx, ptRef.id, user)) {\n      return true\n    }\n  }\n  return false\n}\n\nexport async function checkPackageTeamAccess (\n  ctx: Context,\n  pkgRef: any,\n  user: User = ctx.user,\n) {\n  if (!await hasPackageTeamAccess(ctx, pkgRef, user)) {\n    throw new ApolloError('Unauthorized', ErrorCode.ERROR_UNAUTHORIZED)\n  }\n}\n"
  },
  {
    "path": "packages/backend/src/schema/user/db-types.ts",
    "content": "import { User, UserAccount } from '@/generated/schema'\nimport { values } from 'faunadb'\n\nexport interface DBUserAccount extends UserAccount {\n  userRef: values.Ref\n}\n\nexport interface DBUser extends Omit<User, 'accounts'> {\n  ref: values.Ref\n  accounts: DBUserAccount[]\n  projectTypeBookmarks?: string[]\n  teamsProjectTypes: values.Ref[]\n}\n"
  },
  {
    "path": "packages/backend/src/schema/user/index.ts",
    "content": "import gql from 'graphql-tag'\nimport { Resolvers } from '@/generated/schema'\nimport { ApolloError } from '@nodepack/plugin-apollo'\nimport { ErrorCode } from '@/const/error-codes'\nimport { query as q } from 'faunadb'\nimport { mapDocuments } from '@/util/fauna'\n\nexport const typeDefs = gql`\ntype User {\n  id: ID!\n  nickname: String!\n  email: String!\n  accounts: [UserAccount!]!\n  avatar: String\n  admin: Boolean\n  teams: [Team!]!\n}\n\ntype UserAccount {\n  id: ID!\n  provider: String!\n  profileId: ID!\n  nickname: String\n  profileUrl: String\n}\n\ntype Query {\n  currentUser: User\n  allUsers: [User!]! @admin @auth\n}\n`\n\nexport const resolvers: Resolvers = {\n  User: {\n    accounts: (user, args, ctx) => {\n      if (user !== ctx.user as any) {\n        throw new ApolloError('Unauthorized', ErrorCode.ERROR_UNAUTHORIZED)\n      }\n      return user.accounts\n    },\n\n    teams: async (user, args, ctx) => {\n      const { data } = await ctx.db.query(q.Map(\n        q.Paginate(q.Match(q.Index('teams_by_user'), user.ref), { size: 1000 }),\n        q.Lambda(['ref'], q.Get(q.Var('ref'))),\n      ))\n      return mapDocuments(data)\n    },\n  },\n\n  Query: {\n    // @ts-ignore\n    currentUser: (root, args, ctx) => ctx.user,\n\n    allUsers: async (root, args, ctx) => {\n      const { data } = await ctx.db.query(q.Map(\n        q.Paginate(q.Match(q.Index('all_users')), { size: 10000 }),\n        q.Lambda(['ref'], q.Get(q.Var('ref'))),\n      ))\n      return mapDocuments(data)\n    },\n  },\n}\n"
  },
  {
    "path": "packages/backend/src/shim-ejs.d.ts",
    "content": "declare module '*.ejs' {\n  export default function render (data?: any | null): string\n}\n"
  },
  {
    "path": "packages/backend/src/util/fauna.ts",
    "content": "import { values } from 'faunadb'\n\nexport function mapDocument<TDocument = any> (document: values.Document<TDocument>) {\n  return {\n    id: document.ref.id,\n    ref: document.ref,\n    ts: document.ts,\n    ...document.data,\n  }\n}\n\nexport function mapDocuments<TDocument = any> (documents: Array<values.Document<TDocument>>) {\n  return documents.map(mapDocument)\n}\n"
  },
  {
    "path": "packages/backend/src/util/github.ts",
    "content": "import Octokit from '@octokit/rest'\n\nexport function getPageTotalCount<T> (result: Octokit.Response<T>) {\n  if (!result.headers.link) { return 0 }\n  const links = result.headers.link.split(',').map(\n    (text) => text.split(';').map((part) => part.trim()),\n  )\n  const last = links.find((link) => link[1] === 'rel=\"last\"')\n  if (last) {\n    const [, count] = /page=(\\d+)>/.exec(last[0])\n    return parseInt(count, 10)\n  } else if (Array.isArray(result.data)) {\n    return result.data.length\n  }\n  return 0\n}\n"
  },
  {
    "path": "packages/backend/src/util/metadata.ts",
    "content": "import mem from 'p-memoize'\nimport ms from 'ms'\nimport { Context } from '@/context'\nimport { query as q, values } from 'faunadb'\nimport { DBPackageInterface } from '@/schema/package-interface/db-types'\n\nconst METADATA_MAX_AGE = ms('6h')\n\nconst ALGOLIA_INDEX: { [key: string]: string } = {\n  Packages: 'packages',\n  PackageProposals: null,\n}\n\nexport async function updateMetadata (\n  ctx: Context,\n  ref: values.Ref,\n  type: string,\n  data: any,\n  version: number,\n  additionalData: any = {},\n) {\n  const result = {\n    version,\n    ts: Date.now(),\n    data,\n  }\n  await ctx.db.query(\n    q.Update(\n      ref,\n      {\n        data: {\n          metadata: {\n            [type]: result,\n          },\n          ...additionalData,\n        },\n      },\n    ),\n  )\n  return result\n}\n\nconst NPM_METADATA_VERSION = 6\n\nexport const getNpmMetadata = mem(async (pkg: DBPackageInterface, ctx: Context): Promise<any> => {\n  try {\n    let result = pkg.metadata?.npm\n    if (!result || result.version !== NPM_METADATA_VERSION || Date.now() - result.ts > METADATA_MAX_AGE) {\n      let npmName: string\n\n      if (pkg.dataSources?.npm) {\n        npmName = pkg.dataSources.npm.name\n      } else {\n        npmName = pkg.name\n      }\n\n      const data = await ctx.npm(`/${encodeURIComponent(npmName)}`)\n      console.log('REQUEST npm', npmName)\n      // Add new data props to be saved here\n      // and increment NPM_METADATA_VERSION\n      result = await updateMetadata(ctx, pkg.ref, 'npm', {\n        maintainers: data.maintainers,\n        repository: data.repository,\n        homepage: data.homepage,\n        license: data.license,\n        description: data.description,\n        logo: data.logo,\n        awesomejs: data.awesomejs,\n      }, NPM_METADATA_VERSION, {\n        dataSources: {\n          npm: {\n            name: npmName,\n          },\n        },\n      })\n    }\n    return result.data\n  } catch (e) {\n    console.error(e)\n  }\n  return {\n    maintainers: [],\n  }\n}, {\n  maxAge: ms('1s'),\n  cacheKey: (pkg) => pkg.id,\n})\n\nconst GITHUB_METADATA_VERSION = 7\n\nexport const getGithubDataSource = async (pkg: DBPackageInterface, ctx: Context) => {\n  let owner: string\n  let repo: string\n  if (pkg.dataSources?.github) {\n    owner = pkg.dataSources.github.owner\n    repo = pkg.dataSources.github.repo\n  } else if (pkg.github) {\n    // @TODO legacy\n    owner = pkg.github.owner\n    repo = pkg.github.repo\n\n    await ctx.db.query(\n      q.Update(\n        pkg.ref,\n        {\n          data: {\n            github: null,\n            dataSources: {\n              github: {\n                owner,\n                repo,\n              },\n            },\n          },\n        },\n      ),\n    )\n\n    console.log('Migrated from `github` to `dataSources.github`.')\n  } else {\n    const npmData = await getNpmMetadata(pkg, ctx)\n    let githubUrl\n\n    if (npmData.repository?.type === 'git' && npmData.repository?.url.includes('github.com')) {\n      githubUrl = npmData.repository.url\n    } else if (npmData.bugs?.url.includes('github.com')) {\n      githubUrl = npmData.bugs.url\n    } else if (npmData.homepage?.includes('github.com')) {\n      githubUrl = npmData.homepage\n    }\n\n    if (githubUrl) {\n      const [, o, r] = /github\\.com\\/([a-z0-9_-]+)\\/([a-z0-9_-]+)/i.exec(githubUrl)\n      owner = o\n      repo = r\n\n      await ctx.db.query(\n        q.Update(\n          pkg.ref,\n          {\n            data: {\n              dataSources: {\n                github: {\n                  owner,\n                  repo,\n                },\n              },\n            },\n          },\n        ),\n      )\n    }\n  }\n\n  return {\n    owner,\n    repo,\n  }\n}\n\nexport const getGithubMetadata = mem(async (pkg: DBPackageInterface, ctx: Context): Promise<any> => {\n  try {\n    let result = pkg.metadata?.github\n    if (!result || result.version !== GITHUB_METADATA_VERSION || Date.now() - result.ts > METADATA_MAX_AGE) {\n      let data\n\n      const { owner, repo } = await getGithubDataSource(pkg, ctx)\n\n      if (!repo) {\n        return {}\n      }\n\n      const { data: githubData } = await ctx.github.repos.get({\n        owner,\n        repo,\n      })\n      console.log('REQUEST github', pkg.name)\n      // Add new data props to be saved here\n      // and increment GITHUB_METADATA_VERSION\n      data = {\n        slug: {\n          owner,\n          repo,\n        },\n        stars: githubData.stargazers_count,\n        htmlUrl: githubData.html_url,\n        owner: {\n          avatar: githubData.owner.avatar_url,\n        },\n        description: githubData.description,\n        defaultBranch: githubData.default_branch,\n      }\n\n      const algoliaIndex = ALGOLIA_INDEX[pkg.ref.collection.id]\n      if (algoliaIndex) {\n        const index = ctx.algolia.initIndex(algoliaIndex)\n        await index.partialUpdateObject({\n          objectID: pkg.id,\n          stars: githubData.stargazers_count,\n          defaultLogo: githubData.owner.avatar_url,\n          ...(githubData.description ? {\n            description: githubData.description,\n          } : {}),\n        })\n      }\n      result = await updateMetadata(ctx, pkg.ref, 'github', data, GITHUB_METADATA_VERSION)\n    }\n    return result.data\n  } catch (e) {\n    console.error(e)\n    return {\n      slug: {},\n      owner: {},\n    }\n  }\n}, {\n  maxAge: ms('1s'),\n  cacheKey: (pkg) => pkg.id,\n})\n"
  },
  {
    "path": "packages/backend/src/util/package-index.ts",
    "content": "import { Context } from '@/context'\nimport { query as q } from 'faunadb'\n\nexport async function getIndexObject (\n  ctx: Context,\n  pkg: any,\n) {\n  const npmData = await ctx.npm(`/${encodeURIComponent(pkg.data.name)}`)\n  let githubData\n  if (pkg.data.metadata.github) {\n    const { owner, repo } = pkg.data.metadata.github.data.slug\n    const { data } = await ctx.github.repos.get({\n      owner,\n      repo,\n    })\n    githubData = data\n  } else {\n    githubData = {\n      owner: {},\n    }\n  }\n  const projectType = await ctx.db.query<any>(q.Get(pkg.data.projectTypes[0]))\n  const projectTypes = await ctx.db.query<any[]>(q.Map(\n    pkg.data.projectTypes,\n    q.Lambda(['ref'], q.Get(q.Var('ref'))),\n  ))\n  return {\n    objectID: pkg.ref.id,\n    _tags: pkg.data.info.tags || [],\n    name: pkg.data.name,\n    description: githubData.description || npmData.description,\n    keywords: npmData.keywords,\n    license: npmData.license,\n    maintainers: npmData.maintainers,\n    stars: githubData.stargazers_count || 0,\n    defaultLogo: githubData.owner.avatar_url,\n    projectType: {\n      id: projectType.ref.id,\n      name: projectType.data.name,\n      slug: projectType.data.slug,\n      logo: projectType.data.logo,\n    },\n    projectTypes: projectTypes.map((pt) => ({\n      id: pt.ref.id,\n      name: pt.data.name,\n      slug: pt.data.slug,\n      logo: pt.data.logo,\n    })),\n  }\n}\n\nexport async function indexPackage (\n  ctx: Context,\n  pkg: any,\n) {\n  const index = ctx.algolia.initIndex('packages')\n  return index.addObject(await getIndexObject(ctx, pkg))\n}\n\nexport async function updatePackageIndex (\n  ctx: Context,\n  pkg: any,\n) {\n  const index = ctx.algolia.initIndex('packages')\n  return index.saveObject(await getIndexObject(ctx, pkg))\n}\n"
  },
  {
    "path": "packages/backend/src/util/readme.ts",
    "content": "import { Context } from '@/context'\nimport { getNpmMetadata, getGithubMetadata } from './metadata'\nimport { GithubDataSource } from '@/schema/package-interface/data-source'\n\nexport async function getFileContent (\n  githubDatasource: GithubDataSource,\n  path: string,\n  ctx: Context,\n) {\n  const { data }: { data: string } = await ctx.github.repos.getContents({\n    owner: githubDatasource.owner,\n    repo: githubDatasource.repo,\n    headers: {\n      accept: 'application/vnd.github.3.html',\n    },\n    path,\n  }) as any\n  return data\n}\n\nexport async function getReadmeContent (\n  githubDatasource: GithubDataSource,\n  ctx: Context,\n) {\n  const { data }: { data: string } = await ctx.github.repos.getReadme({\n    owner: githubDatasource.owner,\n    repo: githubDatasource.repo,\n    headers: {\n      accept: 'application/vnd.github.3.html',\n    },\n  }) as any\n  return data\n}\n\nexport async function getReadme (\n  pkg: any,\n  ctx: Context,\n): Promise<string> {\n  if (pkg.dataSources.github) {\n    const npmMetadata = pkg.dataSources.npm ? await getNpmMetadata(pkg, ctx) : null\n    let data: string\n    if (npmMetadata?.repository?.directory) {\n      data = await getFileContent(pkg.dataSources.github, `${npmMetadata.repository.directory}/README.md`, ctx)\n    }\n    if (!data) {\n      data = await getReadmeContent(pkg.dataSources.github, ctx)\n    }\n    const githubMetadata = await getGithubMetadata(pkg, ctx)\n    data = processReadme(pkg.dataSources.github, data, githubMetadata.defaultBranch)\n    return data\n  }\n}\n\nexport function processReadme (\n  githubDatasource: GithubDataSource,\n  text: string,\n  defaultBranch: string,\n) {\n  // Fix image urls\n  text = text.replace(/src=\"([^\"]+)/gi, (result, group1) => {\n    if (group1.startsWith('http')) {\n      return result\n    } else if (group1.startsWith('/')) {\n      return `src=\"https://github.com/${\n        encodeURIComponent(githubDatasource.owner)\n      }/${\n        encodeURIComponent(githubDatasource.repo)\n      }/raw/${defaultBranch}${group1}`\n    } else {\n      return `src=\"https://raw.githubusercontent.com/${\n        encodeURIComponent(githubDatasource.owner)\n      }/${\n        encodeURIComponent(githubDatasource.repo)\n      }/${defaultBranch}/${group1}${group1.endsWith('svg') ? '?sanitize=true' : ''}`\n    }\n  })\n\n  // Fix image sizes\n  text = text.replace(/(width|height)=\"(\\d+)\"/gi, (result, group1, group2) => `${result} style=\"${group1}:${group2}px\"`)\n\n  // Links\n  text = text.replace(/href=\"(.*)\"/gi, (result, group1) => {\n    if (group1.startsWith('#')) {\n      return `href=\"#user-content-${group1.substr(1)}\"`\n    } else {\n      return `${result} target=\"_blank\"`\n    }\n  })\n\n  return text\n}\n"
  },
  {
    "path": "packages/backend/src/util/tag-map.ts",
    "content": "import { Context } from '@/context'\nimport { query as q, values, Expr } from 'faunadb'\n\nexport async function updateProjectTypeTags (projectTypeRef: values.Ref | Expr, ctx: Context) {\n  const { data: packages } = await ctx.db.query(q.Map(\n    q.Paginate(q.Match(q.Index('packages_by_project_type'), projectTypeRef), { size: 100000 }),\n    q.Lambda('ref', q.Get(q.Var('ref'))),\n  ))\n  const counters = new Map<string, number>()\n  for (const pkg of packages) {\n    for (const tag of pkg.data.info.tags) {\n      let count = counters.get(tag)\n      if (!count) {\n        count = 0\n      }\n      count++\n      counters.set(tag, count)\n    }\n  }\n  await ctx.db.query(\n    q.Do(\n      // Reset counters to empty objects\n      q.Update(projectTypeRef, { data: { tagMap: null } }),\n      q.Update(projectTypeRef, { data: { tagMap: Array.from(counters.keys()).reduce((map, key) => {\n        map[key] = counters.get(key)\n        return map\n      }, {} as { [key: string]: number }) } }),\n    ),\n  )\n}\n"
  },
  {
    "path": "packages/backend/src/util/tags.ts",
    "content": "export function sanitizeTags (tags: string[]) {\n  return Array.from(new Set(tags.map((t) => t.trim()).filter((t) => t.length)))\n}\n"
  },
  {
    "path": "packages/backend/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\n        \"src/*\"\n      ],\n      \"@config/*\": [\n        \"config/*\"\n      ],\n      \"@context\": [\n        \"src/generated/context.d.ts\"\n      ]\n    },\n    \"lib\": [\n      \"dom\",\n      \"esnext\"\n    ],\n    \"types\": [\n      \"webpack-env\"\n    ],\n    \"target\": \"es5\",\n    \"module\": \"esnext\",\n    \"importHelpers\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"sourceMap\": true,\n    \"noUnusedLocals\": true,\n    \"skipLibCheck\": true,\n    // Strict\n    \"noImplicitAny\": true,\n    \"noImplicitThis\": true,\n    \"alwaysStrict\": true,\n    \"strictBindCallApply\": true,\n    \"strictFunctionTypes\": true,\n    // May break autocomplete\n    \"strictNullChecks\": false\n  },\n  \"exclude\": [\n    \"node_modules\"\n  ],\n  \"include\": [\n    \"src/**/*.ts\",\n    \"tests/**/*.ts\"\n  ]\n}"
  },
  {
    "path": "packages/backend/tslint.json",
    "content": "{\n  \"defaultSeverity\": \"warning\",\n  \"extends\": [\n    \"tslint:recommended\"\n  ],\n  \"linterOptions\": {\n    \"exclude\": [\n      \"node_modules/**\",\n      \"src/generated/**\"\n    ]\n  },\n  \"rules\": {\n    \"quotemark\": [true, \"single\"],\n    \"indent\": [true, \"spaces\", 2],\n    \"interface-name\": false,\n    \"ordered-imports\": false,\n    \"object-literal-sort-keys\": false,\n    \"no-consecutive-blank-lines\": false,\n    \"semicolon\": [true, \"never\"],\n    \"space-before-function-paren\": [true, \"always\"],\n    \"no-console\": false,\n    \"trailing-comma\": [true, {\"multiline\": \"always\", \"singleline\": \"never\"}]\n  }\n}\n"
  },
  {
    "path": "packages/frontend/.browserslistrc",
    "content": "> 1%\nlast 2 versions\n"
  },
  {
    "path": "packages/frontend/.editorconfig",
    "content": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": "packages/frontend/.eslintignore",
    "content": "node_modules/\n"
  },
  {
    "path": "packages/frontend/.eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  env: {\n    node: true,\n  },\n  'extends': [\n    'plugin:vue/recommended',\n    '@vue/standard',\n  ],\n  rules: {\n    'no-console': process.env.NODE_ENV === 'production' ? ['error', { 'allow': ['error'] }] : 'off',\n    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',\n    'comma-dangle': ['error', 'always-multiline'],\n  },\n  parserOptions: {\n    parser: 'babel-eslint',\n  },\n}\n"
  },
  {
    "path": "packages/frontend/.gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n/tests/e2e/videos/\n/tests/e2e/screenshots/\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "packages/frontend/README.md",
    "content": "# awesomejs-frontend\n\n## Project setup\n```\nyarn install\n```\n\n### Compiles and hot-reloads for development\n```\nyarn run dev\n```\n\n### Compiles and minifies for production\n```\nyarn run build\n```\n\n### Run your tests\n```\nyarn run test\n```\n\n### Lints and fixes files\n```\nyarn run lint\n```\n\n### Run your end-to-end tests\n```\nyarn run test:e2e\n```\n\n### Customize configuration\nSee [Configuration Reference](https://cli.vuejs.org/config/).\n"
  },
  {
    "path": "packages/frontend/apollo.config.js",
    "content": "const path = require('path')\n\n// Load .env files\nconst { loadEnv } = require('vue-cli-plugin-apollo/utils/load-env')\nconst env = loadEnv([\n  path.resolve(__dirname, '.env'),\n  path.resolve(__dirname, '.env.local'),\n])\n\nmodule.exports = {\n  client: {\n    service: {\n      name: 'awesomejs',\n      url: 'http://localhost:4040/graphql',\n    },\n    includes: ['src/**/*.{js,jsx,ts,tsx,vue,gql}'],\n  },\n  engine: {\n    apiKey: env.VUE_APP_APOLLO_ENGINE_KEY,\n  },\n}\n"
  },
  {
    "path": "packages/frontend/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    '@vue/app',\n  ],\n}\n"
  },
  {
    "path": "packages/frontend/cypress.json",
    "content": "{\n  \"pluginsFile\": \"tests/e2e/plugins/index.js\"\n}\n"
  },
  {
    "path": "packages/frontend/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n    },\n  },\n  \"include\": [\n    \"./src/**/*\",\n  ],\n}\n"
  },
  {
    "path": "packages/frontend/package.json",
    "content": "{\n  \"name\": \"frontend\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vue-cli-service serve\",\n    \"build\": \"vue-cli-service build\",\n    \"lint\": \"vue-cli-service lint\",\n    \"test:e2e\": \"vue-cli-service test:e2e\"\n  },\n  \"dependencies\": {\n    \"@awesomejs/shared-utils\": \"^1.0.0\",\n    \"@vue/apollo-composable\": \"4.0.0-alpha.4\",\n    \"@vue/apollo-util\": \"4.0.0-alpha.2\",\n    \"@vue/composition-api\": \"^0.3.4\",\n    \"algoliasearch\": \"^3.35.0\",\n    \"core-js\": \"^3.2.1\",\n    \"d3\": \"^5.15.0\",\n    \"emoji-toolkit\": \"^5.0.5\",\n    \"focus-trap\": \"^5.0.2\",\n    \"focus-trap-vue\": \"^0.0.4\",\n    \"focus-visible\": \"^5.0.2\",\n    \"github-syntax-dark\": \"^0.5.0\",\n    \"lodash\": \"^4.17.15\",\n    \"luxon\": \"^1.21.3\",\n    \"millify\": \"^3.1.2\",\n    \"qrcode\": \"^1.4.2\",\n    \"register-service-worker\": \"^1.6.2\",\n    \"v-tooltip\": \"^3.0.0-alpha.10\",\n    \"vue\": \"^2.6.10\",\n    \"vue-focus\": \"^2.1.0\",\n    \"vue-global-events\": \"^1.1.2\",\n    \"vue-meta\": \"^2.3.1\",\n    \"vue-multiselect\": \"^2.1.6\",\n    \"vue-router\": \"^3.0.3\"\n  },\n  \"devDependencies\": {\n    \"@fullhuman/postcss-purgecss\": \"^1.2.0\",\n    \"@ky-is/vue-cli-plugin-tailwind\": \"^2.0.0\",\n    \"@types/luxon\": \"^1.21.0\",\n    \"@vue/cli-plugin-babel\": \"^4.0.0-rc.7\",\n    \"@vue/cli-plugin-e2e-cypress\": \"^4.0.0-rc.7\",\n    \"@vue/cli-plugin-eslint\": \"^4.0.0-rc.7\",\n    \"@vue/cli-plugin-pwa\": \"^4.0.0-rc.7\",\n    \"@vue/cli-service\": \"^4.0.0-rc.7\",\n    \"@vue/eslint-config-standard\": \"^4.0.0\",\n    \"babel-eslint\": \"^10.0.1\",\n    \"eslint\": \"^5.16.0\",\n    \"eslint-plugin-vue\": \"^5.0.0\",\n    \"graphql-tag\": \"^2.9.0\",\n    \"postcss-nested\": \"^4.1.2\",\n    \"postcss-preset-env\": \"^6.6.0\",\n    \"tailwindcss\": \"^1.0.1\",\n    \"vue-cli-plugin-apollo\": \"^0.21.0\",\n    \"vue-template-compiler\": \"^2.6.10\"\n  },\n  \"engines\": {\n    \"node\": \">=10\"\n  }\n}\n"
  },
  {
    "path": "packages/frontend/postcss.config.js",
    "content": "const IN_PRODUCTION = process.env.NODE_ENV === 'production'\n\nmodule.exports = {\n  plugins: [\n    require('postcss-preset-env')({ stage: 0 }),\n    require('tailwindcss')(),\n    IN_PRODUCTION && require('@fullhuman/postcss-purgecss')({\n      content: [ `./public/**/*.html`, `./src/**/*.vue` ],\n      defaultExtractor (content) {\n        const contentWithoutStyleBlocks = content.replace(/<style[^]+?<\\/style>/gi, '')\n        return contentWithoutStyleBlocks.match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || []\n      },\n      whitelist: [\n        // <router-link/>\n        'a',\n      ],\n      whitelistPatterns: [\n        /-(leave|enter|appear)(|-(to|from|active))$/,\n        /^(?!(|.*?:)cursor-move).+-move$/,\n        /^router-link(|-exact)-active$/,\n        /^v-popper/,\n      ],\n    }),\n    require('autoprefixer')(),\n    require('postcss-nested')(),\n  ],\n}\n"
  },
  {
    "path": "packages/frontend/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <meta property=\"og:title\" data-vmid=\"og:title\" content=\"Awesome JS Packages\">\n    <meta property=\"og:description\" data-vmid=\"og:description\" content=\"Find awesome package for your actual project\">\n    <meta property=\"og:image\" data-vmid=\"og:image\" content=\"<%= BASE_URL %>thumbnail.png\">\n    <meta name=\"twitter:card\" content=\"summary_large_image\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\">\n    <link rel=\"shortcut icon\" type=\"image/png\" href=\"<%= BASE_URL %>favicon.png\">\n    <link rel=\"manifest\" href=\"<%= BASE_URL %>manifest.json\">\n    <title>Awesome JS</title>\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but Awesome JS doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "packages/frontend/public/robots.txt",
    "content": "User-agent: *\nDisallow:\n"
  },
  {
    "path": "packages/frontend/src/assets/styles/components/document.postcss",
    "content": ".about-document {\n  max-width: 768px;\n  @apply px-12 mx-auto my-32;\n\n  h1 .text-lg {\n    @apply text-3xl;\n  }\n\n  h2 {\n    @apply text-2xl mt-24 mb-4 text-yellow-400;\n  }\n\n  h3 {\n    @apply text-xl mt-12 mb-4 text-yellow-500;\n  }\n\n  p {\n    @apply mt-4 mb-6;\n  }\n\n  @media (max-width: 640px) {\n    @apply px-4 my-12;\n\n    h2 {\n      @apply mt-12;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/assets/styles/components/grids.postcss",
    "content": ".project-types-grid {\n  display: grid;\n  /* @TODO use theme values */\n  grid-template-columns: repeat(auto-fit, 128px);\n  grid-gap: 48px;\n  justify-items: center;\n\n  @media (max-width: 1023px) {\n    grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));\n    grid-gap: 16px;\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/assets/styles/components/markdown.postcss",
    "content": "/* purgecss start ignore */\n.markdown {\n  @apply overflow-hidden;\n\n  img {\n    @apply inline-block;\n\n    &.ally-bg {\n      @apply bg-gray-200 rounded p-2;\n\n      &.avatar {\n        @apply p-0 rounded-lg;\n      }\n    }\n\n    &[align=\"left\"] {\n      @apply mr-5;\n    }\n\n    &[align=\"right\"] {\n      @apply ml-5;\n    }\n  }\n\n  a {\n    @apply text-purple-500;\n\n    &:hover {\n      @apply text-purple-400;\n    }\n  }\n\n  h1 {\n    @apply text-3xl mt-8 mb-4 text-gray-600;\n\n    @media (max-width: 640px) {\n      @apply text-2xl;\n    }\n  }\n\n  h2 {\n    @apply text-2xl;\n\n    @media (max-width: 640px) {\n      @apply text-xl;\n    }\n  }\n\n  h3 {\n    @apply text-xl;\n\n    @media (max-width: 640px) {\n      @apply text-lg;\n    }\n  }\n\n  h1, h2 {\n    @apply pb-2 border-b border-gray-800;\n  }\n\n  h2, h3 {\n    @apply mt-8 mb-3 text-gray-500;\n  }\n\n  h4, h5, h6 {\n    @apply text-lg mt-6;\n\n    @media (max-width: 640px) {\n      @apply text-base;\n    }\n  }\n\n  h1, h2, h3, h4, h5, h6 {\n    @apply flex flex-wrap items-center whitespace-pre-wrap font-bold;\n\n    &:first-child {\n      @apply mt-0;\n    }\n\n    &[align=\"center\"] {\n      @apply justify-center;\n    }\n\n    &[align=\"right\"] {\n      @apply justify-end;\n    }\n\n    &:hover {\n      .anchor {\n        @apply visible;\n      }\n    }\n  }\n\n  p {\n    @apply my-4;\n  }\n\n  table {\n    border-collapse: collapse;\n  }\n\n  tr {\n    &:nth-child(2n) {\n      @apply bg-gray-850;\n    }\n  }\n\n  td, th {\n    @apply border border-gray-800 p-2;\n  }\n\n  hr {\n    @apply border-gray-800;\n  }\n\n  ul {\n    @apply list-disc;\n  }\n\n  ol {\n    @apply list-decimal;\n  }\n\n  ul, ol {\n    @apply my-2 pl-2;\n  }\n\n  li {\n    @apply ml-6;\n  }\n\n  pre {\n    @apply text-white bg-gray-850 rounded p-4 my-4 overflow-x-auto;\n    font-size: 14px;\n  }\n\n  code {\n    @apply text-white bg-gray-850 rounded px-2 py-1 break-words;\n  }\n\n  blockquote {\n    @apply pl-4 my-4 border-l-4 border-purple-700 text-gray-500;\n\n    p {\n      &:first-child {\n        @apply mt-0;\n      }\n\n      &:last-child {\n        @apply mb-0;\n      }\n    }\n  }\n\n  .markdown-body {\n    @apply p-0;\n  }\n\n  .anchor {\n    @apply invisible text-gray-600 pr-1 -ml-5 h-6 flex items-center;\n    svg {\n      @apply fill-current;\n    }\n  }\n\n  .pl-md {\n    @apply text-red-300 bg-red-900 py-1;\n  }\n\n  .pl-mi1 {\n    @apply text-green-300 bg-green-900 py-1;\n  }\n}\n/* purgecss end ignore */\n"
  },
  {
    "path": "packages/frontend/src/assets/styles/components/multi-select.postcss",
    "content": "/* purgecss start ignore */\n\nfieldset[disabled] .multiselect {\n  pointer-events: none;\n}\n\n.multiselect__spinner {\n  position: absolute;\n  right: 1px;\n  top: 1px;\n  width: 48px;\n  height: 35px;\n  background: #fff;\n  display: block;\n\n  &:before,\n  &:after {\n    position: absolute;\n    content: \"\";\n    top: 50%;\n    left: 50%;\n    margin: -8px 0 0 -8px;\n    width: 16px;\n    height: 16px;\n    border-radius: 100%;\n    border-color: #41b883 transparent transparent;\n    border-style: solid;\n    border-width: 2px;\n    box-shadow: 0 0 0 1px transparent;\n  }\n\n  &:before {\n    animation: spinning 2.4s cubic-bezier(0.41, 0.26, 0.2, 0.62);\n    animation-iteration-count: infinite;\n  }\n\n  &:after {\n    animation: spinning 2.4s cubic-bezier(0.51, 0.09, 0.21, 0.8);\n    animation-iteration-count: infinite;\n  }\n}\n\n.multiselect__loading-enter-active,\n.multiselect__loading-leave-active {\n  transition: opacity 0.4s ease-in-out;\n  opacity: 1;\n}\n\n.multiselect__loading-enter,\n.multiselect__loading-leave-active {\n  opacity: 0;\n}\n\n.multiselect,\n.multiselect__input,\n.multiselect__single {\n  font-family: inherit;\n  font-size: 16px;\n  touch-action: manipulation;\n}\n\n.multiselect {\n  box-sizing: content-box;\n  display: block;\n  position: relative;\n  width: 100%;\n  min-height: 40px;\n  text-align: left;\n\n  * {\n    box-sizing: border-box;\n  }\n\n  &:focus {\n    outline: none;\n  }\n}\n\n.multiselect--disabled {\n  background: #ededed;\n  pointer-events: none;\n  opacity: 0.6;\n}\n\n.multiselect--active {\n  z-index: 50;\n}\n\n.multiselect--active:not(.multiselect--above) .multiselect__current,\n.multiselect--active:not(.multiselect--above) .multiselect__input,\n.multiselect--active:not(.multiselect--above) .multiselect__tags {\n  border-bottom-left-radius: 0;\n  border-bottom-right-radius: 0;\n}\n\n.multiselect--active .multiselect__select {\n  transform: rotateZ(180deg);\n}\n\n.multiselect--above.multiselect--active .multiselect__current,\n.multiselect--above.multiselect--active .multiselect__input,\n.multiselect--above.multiselect--active .multiselect__tags {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n\n.multiselect__input,\n.multiselect__single {\n  position: relative;\n  display: inline-block;\n  min-height: 20px;\n  line-height: 20px;\n  border: none;\n  padding-left: 12px;\n  padding-top: 2px;\n  background: none;\n  width: calc(100%);\n  transition: border 0.1s ease;\n  box-sizing: border-box;\n  margin-bottom: 8px;\n  vertical-align: top;\n}\n\n.multiselect__input::placeholder {\n  @apply text-gray-300;\n}\n\n.multiselect__tag ~ .multiselect__input,\n.multiselect__tag ~ .multiselect__single {\n  width: auto;\n}\n\n.multiselect__input:hover,\n.multiselect__single:hover {\n  border-color: #cfcfcf;\n}\n\n.multiselect__input:focus,\n.multiselect__single:focus {\n  border-color: #a8a8a8;\n  outline: none;\n}\n\n.multiselect__single {\n  padding-left: 5px;\n  margin-bottom: 8px;\n}\n\n.multiselect__tags-wrap {\n  display: inline;\n}\n\n.multiselect__tags {\n  min-height: 56px;\n  @apply block bg-gray-800 rounded pl-4 pt-4 pb-1;\n\n  &:hover {\n    @apply bg-gray-700;\n  }\n}\n\n.multiselect__tag {\n  padding: 4px 26px 4px 10px;\n  margin-right: 10px;\n  line-height: 1;\n  margin-bottom: 5px;\n  @apply truncate max-w-full rounded text-white bg-blue-700 relative inline-block;\n}\n\n.multiselect__tag-icon {\n  margin-left: 7px;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  font-weight: 700;\n  font-style: initial;\n  width: 22px;\n  line-height: 22px;\n  transition: all 0.2s ease;\n  @apply rounded text-center absolute cursor-pointer;\n\n  &:after {\n    content: \"×\";\n    font-size: 14px;\n    @apply text-blue-300;\n  }\n\n  &:focus,\n  &:hover {\n    @apply bg-blue-600;\n\n    &:after {\n      @apply text-white;\n    }\n  }\n}\n\n.multiselect__current {\n  line-height: 16px;\n  min-height: 40px;\n  box-sizing: border-box;\n  display: block;\n  overflow: hidden;\n  padding: 8px 12px 0;\n  padding-right: 30px;\n  white-space: nowrap;\n  margin: 0;\n  text-decoration: none;\n  border-radius: 5px;\n  border: 1px solid #e8e8e8;\n  cursor: pointer;\n}\n\n.multiselect__select {\n  line-height: 16px;\n  display: block;\n  position: absolute;\n  box-sizing: border-box;\n  width: 40px;\n  height: 52px;\n  right: 1px;\n  top: 1px;\n  padding: 4px 8px;\n  margin: 0;\n  text-decoration: none;\n  text-align: center;\n  cursor: pointer;\n  transition: transform 0.2s ease;\n\n  &:before {\n    position: relative;\n    right: 0;\n    top: 65%;\n    color: #999;\n    margin-top: 4px;\n    border-style: solid;\n    border-width: 5px 5px 0 5px;\n    border-color: #999999 transparent transparent transparent;\n    content: \"\";\n  }\n}\n\n.multiselect__placeholder {\n  color: #adadad;\n  display: inline-block;\n  margin-bottom: 10px;\n  padding-left: 12px;\n}\n\n.multiselect--active .multiselect__placeholder {\n  display: none;\n}\n\n.multiselect__content-wrapper {\n  @apply bg-gray-900 w-full border-gray-800 border z-50 overflow-auto block absolute;\n  max-height: 240px;\n  border-top: none;\n  border-bottom-left-radius: 5px;\n  border-bottom-right-radius: 5px;\n  -webkit-overflow-scrolling: touch;\n}\n\n.multiselect__content {\n  list-style: none;\n  display: inline-block;\n  padding: 0;\n  margin: 0;\n  min-width: 100%;\n  vertical-align: top;\n}\n\n.multiselect--above .multiselect__content-wrapper {\n  bottom: 100%;\n  border-bottom-left-radius: 0;\n  border-bottom-right-radius: 0;\n  border-top-left-radius: 5px;\n  border-top-right-radius: 5px;\n  border-bottom: none;\n  border-top: 1px solid theme('colors.gray.800');\n}\n\n.multiselect__content::webkit-scrollbar {\n  display: none;\n}\n\n.multiselect__element {\n  display: block;\n}\n\n.multiselect__option {\n  display: block;\n  padding: 12px;\n  min-height: 40px;\n  line-height: 16px;\n  text-decoration: none;\n  text-transform: none;\n  vertical-align: middle;\n  position: relative;\n  cursor: pointer;\n  white-space: nowrap;\n\n  &:after {\n    top: 0;\n    right: 0;\n    position: absolute;\n    line-height: 40px;\n    padding-right: 12px;\n    padding-left: 20px;\n    font-size: 13px;\n  }\n}\n\n.multiselect__option--highlight {\n  @apply bg-gray-800;\n  outline: none;\n\n  &:after {\n    content: attr(data-select);\n  }\n}\n\n.multiselect__option--selected {\n  @apply bg-blue-500 text-white font-bold;\n\n  &:after {\n    content: attr(data-selected);\n    @apply text-blue-300;\n  }\n\n  &.multiselect__option--highlight {\n    @apply bg-red-700 text-white;\n\n    &:after {\n      content: attr(data-deselect);\n      @apply text-red-100;\n    }\n  }\n}\n\n.multiselect--disabled .multiselect__current,\n.multiselect--disabled .multiselect__select {\n  background: #ededed;\n  color: #a6a6a6;\n}\n\n.multiselect__option--disabled {\n  background: #ededed !important;\n  color: #a6a6a6 !important;\n  cursor: text;\n  pointer-events: none;\n}\n\n.multiselect__option--group {\n  background: #ededed;\n  color: #35495e;\n\n  &.multiselect__option--highlight {\n    background: #35495e;\n    color: #fff;\n\n    &:after {\n      background: #35495e;\n    }\n  }\n}\n\n.multiselect__option--disabled.multiselect__option--highlight {\n  background: #dedede;\n}\n\n.multiselect__option--group-selected.multiselect__option--highlight {\n  background: #ff6a6a;\n  color: #fff;\n\n  &:after {\n    background: #ff6a6a;\n    content: attr(data-deselect);\n    color: #fff;\n  }\n}\n\n.multiselect-enter-active,\n.multiselect-leave-active {\n  transition: all 0.15s ease;\n}\n\n.multiselect-enter,\n.multiselect-leave-active {\n  opacity: 0;\n}\n\n.multiselect__strong {\n  margin-bottom: 8px;\n  line-height: 20px;\n  display: inline-block;\n  vertical-align: top;\n}\n\n*[dir=\"rtl\"] .multiselect {\n  text-align: right;\n}\n\n*[dir=\"rtl\"] .multiselect__select {\n  right: auto;\n  left: 1px;\n}\n\n*[dir=\"rtl\"] .multiselect__tags {\n  padding: 8px 8px 0px 40px;\n}\n\n*[dir=\"rtl\"] .multiselect__content {\n  text-align: right;\n}\n\n*[dir=\"rtl\"] .multiselect__option:after {\n  right: auto;\n  left: 0;\n}\n\n*[dir=\"rtl\"] .multiselect__clear {\n  right: auto;\n  left: 12px;\n}\n\n*[dir=\"rtl\"] .multiselect__spinner {\n  right: auto;\n  left: 1px;\n}\n\n@keyframes spinning {\n  from {\n    transform: rotate(0);\n  }\n  to {\n    transform: rotate(2turn);\n  }\n}\n\n/* purgecss end ignore */"
  },
  {
    "path": "packages/frontend/src/assets/styles/components/popper.postcss",
    "content": "/* purgecss start ignore */\n\n.v-popper {\n  .v-popper__trigger {\n    /* Fix height */\n    line-height: 0;\n  }\n}\n\n.v-popper__popper {\n  &.v-popper--theme-tooltip {\n    .v-popper__inner {\n      @apply bg-white text-gray-900;\n    }\n\n    .v-popper__arrow {\n      @apply border-white;\n    }\n  }\n\n  &.v-popper--theme-dropdown {\n    .v-popper__inner {\n      @apply p-0 bg-gray-900;\n      color: inherit;\n      box-shadow: 0 42px 64px rgba(0, 0, 0, .3), 0 0 12px rgba(0, 0, 0, .3);\n    }\n\n    .v-popper__arrow {\n      @apply border-gray-900;\n    }\n\n    .v-popper__wrapper {\n      &.animate {\n        animation: dropdown-animation .15s cubic-bezier(0, 1, .5, 1);\n      }\n    }\n  }\n\n  &.v-popper--theme-yellow-arrow {\n    .v-popper__arrow {\n      @apply border-yellow-900;\n    }\n  }\n}\n\n\n@keyframes dropdown-animation {\n  0% {\n    transform: scale(.6);\n  }\n  100% {\n    transform: none;\n  }\n}\n\n/* App backdrop when popper is open */\n\n#app {\n  &::before {\n    @apply fixed inset-0 bg-black pointer-events-none opacity-0;\n    content: '';\n    z-index: 50;\n    transition: opacity .15s linear;\n  }\n}\n\n.v-popper {\n  transition: z-index 0s 1s;\n  z-index: 1;\n}\n\nbody.popper-open {\n  #app {\n    &::before {\n      opacity: .5;\n    }\n  }\n\n  .v-popper {\n    transition: none;\n    &.v-popper--open {\n      z-index: 51;\n    }\n  }\n}\n\n/* purgecss end ignore */\n"
  },
  {
    "path": "packages/frontend/src/assets/styles/main.postcss",
    "content": "@import 'https://fonts.googleapis.com/icon?family=Material+Icons';\n/* purgecss ignore */\n@import '~github-syntax-dark/lib/github-dark.css';\n\n/* purgecss start ignore */\nhtml,\nbody {\n  @apply bg-gray-900 text-gray-300;\n  scrollbar-color: theme('colors.gray.800') theme('colors.gray.900');\n}\n\nbody {\n  overflow-x: hidden;\n}\n\n.material-icons {\n  vertical-align: sub;\n}\n\n[type=\"button\"],\n[type=\"submit\"],\n.button-like {\n  -webkit-appearance: none !important;\n  outline: none !important;\n\n  &:active {\n    filter: brightness(90%);\n  }\n\n  &:focus-visible {\n    @apply shadow-outline;\n  }\n}\n\n/* Disable touch blue rectangle */\n[type=\"button\"],\n[type=\"submit\"],\n.button-like,\na {\n  -webkit-touch-callout: none;\n  -webkit-tap-highlight-color: transparent;\n}\n\n*::-webkit-scrollbar-track {\n  background-color: theme('colors.gray.900');\n}\n\n*::-webkit-scrollbar {\n  width: 6px;\n}\n\n*::-webkit-scrollbar-thumb {\n  background-color: theme('colors.gray.800');\n  border: theme('colors.gray.900') 1px solid;\n  border-radius: 2px;\n}\n\n.bg-blur {\n  backdrop-filter: blur(24px);\n}\n\n.no-scroll {\n  @apply overflow-y-hidden;\n}\n\n/* purgecss end ignore */\n"
  },
  {
    "path": "packages/frontend/src/assets/styles/tailwind.postcss",
    "content": "@tailwind base;\n\n/**\n * This injects any component classes registered by plugins.\n */\n@tailwind components;\n\n/**\n * Here you would add any of your custom component classes; stuff that you'd\n * want loaded *before* the utilities so that the utilities could still\n * override them.\n *\n * Example:\n * @import 'components/buttons';\n */\n\n@import './components/grids.postcss';\n@import './components/markdown.postcss';\n@import './components/document.postcss';\n@import './components/popper.postcss';\n@import './components/multi-select.postcss';\n\n/**\n * This injects all of Tailwind's utility classes, generated based on your\n * config file.\n */\n@tailwind utilities;\n\n/**\n * Here you would add any custom utilities you need that don't come out of the\n * box with Tailwind.\n *\n * Example:\n * @import 'utilities/background-patterns';\n */\n\n@import './main.postcss';\n@import './transitions.postcss';\n"
  },
  {
    "path": "packages/frontend/src/assets/styles/transitions.postcss",
    "content": ".fade-enter-active,\n.fade-leave-active,\n.zoom-enter-active,\n.zoom-leave-active {\n  transition: opacity .1s linear;\n}\n\n.fade-enter,\n.fade-leave-to,\n.zoom-enter,\n.zoom-leave-to {\n  opacity: 0;\n}\n\n.zoom-enter-active,\n.zoom-leave-active {\n  .zoomable {\n    transition: transform .1s ease-out;\n  }\n}\n \n.zoom-enter,\n.zoom-leave-to {\n  .zoomable {\n    transform: scale(.9);\n  }\n}\n\n/* Page */\n\n.page-zoom-child-enter-active,\n.page-zoom-child-leave-active,\n.page-zoom-parent-enter-active,\n.page-zoom-parent-leave-active {\n  transition: opacity .2s cubic-bezier(0, 1, .5, 1), transform .4s cubic-bezier(0, 1, .5, 1);\n  transform-origin: calc(50vw) calc(50vh);\n  width: 100vw;\n  height: 100vh;\n  overflow: hidden;\n  position: absolute;\n  z-index: 0;\n}\n\n.page-zoom-child-enter-active {\n  z-index: 1;\n  background: theme('colors.gray.900');\n}\n\n.page-zoom-child-enter,\n.page-zoom-child-leave-to,\n.page-zoom-parent-enter,\n.page-zoom-parent-leave-to {\n  opacity: 0;\n}\n\n.page-zoom-child-enter,\n.page-zoom-parent-leave-to {\n  transform: scale(.9);\n}\n\n.page-zoom-child-leave-to,\n.page-zoom-parent-enter {\n  transform: scale(1.1);\n}\n"
  },
  {
    "path": "packages/frontend/src/cache.js",
    "content": "import { toIdValue } from 'apollo-utilities'\nimport { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'\nimport gql from 'graphql-tag'\nimport introspectionQueryResultData from 'backend/schema-fragment-matcher'\n\nconst fragmentMatcher = new IntrospectionFragmentMatcher({\n  introspectionQueryResultData,\n})\n\nexport const cache = new InMemoryCache({\n  fragmentMatcher,\n  cacheRedirects: {\n    Query: {\n      projectTypeBySlug: (_, { slug }) => {\n        try {\n          // If we already have loaded the list of project types\n          // we want to redirect to the corresponding project type\n          // instead of doing an unnecessary request to the API\n          const { projectTypes } = cache.readQuery({\n            query: gql`query ReachProjectTypesCache {\n              projectTypes {\n                id\n                slug\n              }\n            }`,\n          })\n          // Lookup for the project type with corresponding slug\n          if (projectTypes) {\n            const p = projectTypes.find(t => t.slug === slug)\n            if (p) {\n              // Cache redirect\n              return toIdValue(cache.config.dataIdFromObject({ __typename: 'ProjectType', id: p.id }))\n            }\n          }\n        } catch (e) {\n          // https://github.com/apollographql/apollo-client/issues/1542\n        }\n      },\n\n      projectType: (_, { id }) => toIdValue(cache.config.dataIdFromObject({ __typename: 'ProjectType', id })),\n\n      package: (_, { id }) => toIdValue(cache.config.dataIdFromObject({ __typename: 'Package', id })),\n\n      packageProposal: (_, { id }) => toIdValue(cache.config.dataIdFromObject({ __typename: 'PackageProposal', id })),\n    },\n  },\n})\n"
  },
  {
    "path": "packages/frontend/src/components/App.vue",
    "content": "<script>\nimport AppHeader from './app/AppHeader.vue'\nimport AppFooter from './app/AppFooter.vue'\nimport AppGlobalLoading from './app/AppGlobalLoading.vue'\nimport AppServiceWorkerManager from './app/AppServiceWorkerManager.vue'\nimport PackageInstallationManager from './pkg/PackageInstallationManager.vue'\nimport { watch, ref } from '@vue/composition-api'\nimport { useResponsive } from '@/util/responsive'\nimport { computeDepthWeight } from '@/util/router'\n\nconst title = 'Awesome JS'\n\nexport default {\n  components: {\n    AppHeader,\n    AppFooter,\n    AppGlobalLoading,\n    AppServiceWorkerManager,\n    PackageInstallationManager,\n  },\n\n  setup (props, { root }) {\n    const transitionName = ref()\n\n    const { mobile } = useResponsive()\n\n    // Mobile page transition\n    watch(() => root.$route, (value, oldValue) => {\n      if (!mobile.value) {\n        transitionName.value = null\n      } else {\n        const newDepthWeight = computeDepthWeight(value)\n        const oldDepthWeight = computeDepthWeight(oldValue)\n        if (newDepthWeight < oldDepthWeight) {\n          transitionName.value = 'page-zoom-parent'\n        } else if (newDepthWeight > oldDepthWeight) {\n          transitionName.value = 'page-zoom-child'\n        } else {\n          transitionName.value = null\n        }\n      }\n    }, { lazy: true, flush: 'pre' })\n    const transitionIsActive = ref(false)\n    watch(transitionIsActive, value => {\n      if (value) {\n        document.body.classList.add('overflow-hidden', 'absolute', 'w-screen', 'h-screen')\n        document.documentElement.classList.add('overflow-hidden')\n      } else {\n        document.body.classList.remove('overflow-hidden', 'absolute', 'w-screen', 'h-screen')\n        document.documentElement.classList.remove('overflow-hidden')\n      }\n    })\n\n    return {\n      transitionName,\n      transitionIsActive,\n    }\n  },\n\n  metaInfo: {\n    title,\n    titleTemplate: t => t === title ? t : `${t} - ${title}`,\n    meta: [\n      {\n        property: 'og:title',\n        content: 'Awesome JS Packages',\n        vmid: 'og:title',\n      },\n      {\n        property: 'og:description',\n        content: 'Find awesome package for your actual project',\n        vmid: 'og:description',\n      },\n      {\n        property: 'og:image',\n        content: `${process.env.BASE_URL}thumbnail.png`,\n        vmid: 'og:image',\n      },\n    ],\n  },\n}\n</script>\n\n<template>\n  <div id=\"app\">\n    <AppGlobalLoading />\n\n    <transition\n      :name=\"transitionName\"\n      @before-enter=\"transitionIsActive = true\"\n      @after-enter=\"transitionIsActive = false\"\n    >\n      <div :key=\"transitionName ? $route.fullPath : 'static'\">\n        <AppHeader />\n\n        <div class=\"main-view px-4 my-4 lg:px-16 lg:my-8\">\n          <router-view />\n        </div>\n\n        <AppFooter />\n      </div>\n    </transition>\n\n    <AppServiceWorkerManager />\n    <PackageInstallationManager />\n  </div>\n</template>\n\n<style lang=\"postcss\">\n@import '~@/assets/styles/tailwind.postcss';\n\n.main-view {\n  min-height: calc(100vh - 390px);\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/BaseButton.vue",
    "content": "<script>\nimport SubmitAnimation from './SubmitAnimation.vue'\nimport { computed } from '@vue/composition-api'\n\nexport default {\n  components: {\n    SubmitAnimation,\n    CustomRouterLink: {\n      functional: true,\n      render (h, { data, children, listeners }) {\n        if (listeners['!click']) {\n          data.nativeOn = data.nativeOn || {}\n          data.nativeOn['!click'] = listeners['!click']\n        }\n        return h('router-link', data, children)\n      },\n    },\n  },\n\n  inheritAttrs: false,\n\n  props: {\n    iconLeft: {\n      type: String,\n      default: null,\n    },\n\n    iconRight: {\n      type: String,\n      default: null,\n    },\n\n    type: {\n      type: String,\n      default: 'button',\n    },\n\n    loading: {\n      type: Boolean,\n      default: false,\n    },\n\n    disabled: {\n      type: Boolean,\n      default: false,\n    },\n\n    align: {\n      type: String,\n      default: 'center',\n    },\n\n    square: {\n      type: Boolean,\n      default: false,\n    },\n  },\n\n  setup (props, { attrs, emit }) {\n    const component = computed(() => {\n      if (attrs.to) {\n        return 'CustomRouterLink'\n      } else if (attrs.href) {\n        return 'a'\n      } else {\n        return 'button'\n      }\n    })\n\n    const ghost = computed(() => props.disabled || props.loading)\n\n    function handleClick (event) {\n      if (ghost.value) {\n        event.preventDefault()\n        event.stopPropagation()\n        event.stopImmediatePropagation()\n      } else {\n        emit('click', event)\n      }\n    }\n\n    return {\n      component,\n      ghost,\n      handleClick,\n    }\n  },\n}\n</script>\n\n<template>\n  <component\n    :is=\"component\"\n    v-bind=\"$attrs\"\n    :type=\"type\"\n    :tabindex=\"ghost ? -1 : 0\"\n    role=\"button\"\n    :aria-disabled=\"ghost\"\n    class=\"inline-block cursor-pointer relative select-none outline-none\"\n    :class=\"{\n      'pointer-events-none opacity-75': ghost,\n      'text-center': align === 'center',\n      'rounded': !square,\n    }\"\n    @click.capture=\"handleClick\"\n  >\n    <div\n      class=\"flex items-center rounded\"\n      :class=\"{\n        'opacity-0': loading,\n        'opacity-50': !loading && ghost,\n        'justify-center': align === 'center',\n      }\"\n    >\n      <i\n        v-if=\"iconLeft\"\n        class=\"material-icons text-lg\"\n        :class=\"{\n          'mr-2': $slots.default,\n        }\"\n      >{{ iconLeft }}</i>\n      <slot />\n      <i\n        v-if=\"iconRight\"\n        class=\"material-icons text-lg\"\n        :class=\"{\n          'ml-2': $slots.default,\n        }\"\n      >{{ iconRight }}</i>\n    </div>\n\n    <SubmitAnimation\n      v-if=\"loading\"\n      class=\"absolute inset-0\"\n    />\n  </component>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/BasePopper.vue",
    "content": "<script>\nimport { ref, watch, getCurrentInstance, onUnmounted } from '@vue/composition-api'\n\nconst shownUids = new Set()\n\nexport default {\n  inheritAttrs: false,\n\n  setup () {\n    const popper = ref()\n\n    const shown = ref()\n    const vm = getCurrentInstance()\n\n    /* Animation */\n\n    function triggerHide () {\n      shownUids.delete(vm._uid)\n      if (!shownUids.size) {\n        document.body.classList.remove('popper-open')\n      }\n    }\n\n    watch(shown, value => {\n      if (value) {\n        shownUids.add(vm._uid)\n        document.body.classList.add('popper-open')\n        animate()\n      } else {\n        triggerHide()\n      }\n    }, { lazy: true })\n\n    onUnmounted(triggerHide)\n\n    function animate () {\n      const trigger = popper.value.$refs.trigger\n      const triggerBounds = trigger.getBoundingClientRect()\n\n      const popperContent = popper.value.$refs.popperContent\n      const wrapper = popperContent.$el.querySelector('.v-popper__wrapper')\n      wrapper.classList.remove('animate')\n      const wrapperBounds = wrapper.getBoundingClientRect()\n\n      // Compute popover wrapper <div> transformOrigin\n      const x = (triggerBounds.left + triggerBounds.width / 2) - wrapperBounds.left\n      const y = (triggerBounds.top + triggerBounds.height / 2) - wrapperBounds.top\n\n      wrapper.style.transformOrigin = `${x}px ${y}px`\n      requestAnimationFrame(() => {\n        wrapper.classList.add('animate')\n      })\n    }\n\n    return {\n      popper,\n      shown,\n    }\n  },\n}\n</script>\n\n<template>\n  <VDropdown\n    ref=\"popper\"\n    v-bind=\"$attrs\"\n    v-on=\"$listeners\"\n    @apply-show=\"shown = true\"\n    @apply-hide=\"shown = false\"\n  >\n    <slot />\n    <template #popper>\n      <slot name=\"popper\" />\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/EmptyMessage.vue",
    "content": "<script>\nexport default {\n  props: {\n    icon: {\n      type: String,\n      default: 'filter_list',\n    },\n  },\n}\n</script>\n\n<template>\n  <div class=\"p-8 flex flex-col items-center justify-center text-center\">\n    <i class=\"material-icons mb-2 text-4xl text-gray-700\">{{ icon }}</i>\n\n    <div class=\"text-gray-600\">\n      <slot />\n    </div>\n\n    <div\n      v-if=\"$slots.cta\"\n      class=\"mt-8\"\n    >\n      <slot name=\"cta\" />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/ErrorMessage.vue",
    "content": "<script>\nimport { computed } from '@vue/composition-api'\n\nexport default {\n  props: {\n    error: {\n      type: [String, Object, Error],\n      default: null,\n    },\n  },\n\n  setup (props) {\n    const message = computed(() => {\n      if (typeof props.error === 'string') {\n        return props.error\n      }\n      if (props.error.graphQLErrors) {\n        return props.error.graphQLErrors.map(e => e.message).join('\\n')\n      }\n      return props.error.message\n    })\n\n    return {\n      message,\n    }\n  },\n}\n</script>\n\n<template>\n  <div v-if=\"error\">\n    <i class=\"material-icons text-lg mr-2\">error</i>\n    <span class=\"whitespace-pre\">{{ message }}</span>\n  </div>\n</template>\n\n<style lang=\"postcss\">\n.error-box {\n  @apply p-4 text-red-500 border-2 border-red-900 rounded;\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/LoadingIndicator.vue",
    "content": "<template>\n  <div class=\"flex items-center justify-center\">\n    <div class=\"animation\">\n      <i class=\"material-icons\">cloud</i>\n    </div>\n  </div>\n</template>\n\n<style lang=\"postcss\" scoped>\n.animation {\n  animation: fade-in .5s .3s backwards, loading 2s infinite ease-in-out;\n}\n\n@keyframes fade-in {\n  0% {\n    opacity: 0;\n  }\n  100% {\n    opacity: 1;\n  }\n}\n\n@keyframes loading {\n  0%, 100% {\n    transform: scale(.7);\n  }\n  50% {\n    transform: scale(1);\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/PageTitle.vue",
    "content": "<script>\nexport default {\n  props: {\n    backTo: {\n      type: [String, Object],\n      default: null,\n    },\n\n    center: {\n      type: Boolean,\n      default: false,\n    },\n  },\n}\n</script>\n\n<template>\n  <h1\n    class=\"flex items-center\"\n    :class=\"{\n      'justify-center': center,\n    }\"\n  >\n    <div\n      class=\"flex-none flex items-center\"\n      :class=\"{\n        'justify-center': center,\n        'mr-1 lg:mr-4': !center,\n      }\"\n    >\n      <router-link\n        v-if=\"backTo\"\n        :to=\"backTo\"\n        class=\"mr-2 lg:mr-4 text-gray-600 hover:text-gray-500 h-6\"\n      >\n        <i class=\"material-icons text-xl lg:text-2xl\">arrow_back</i>\n      </router-link>\n\n      <span class=\"text-gray-500 text-xl lg:text-2xl leading-snug\">\n        <slot />\n      </span>\n    </div>\n\n    <slot name=\"after\" />\n  </h1>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/PopupModal.vue",
    "content": "<script>\nimport { FocusTrap } from 'focus-trap-vue'\nimport { useLockScroll } from '@/util/lock-scroll'\n\nexport default {\n  components: {\n    FocusTrap,\n  },\n\n  props: {\n    size: {\n      type: String,\n      default: 'full',\n    },\n  },\n\n  setup (props, { emit }) {\n    useLockScroll()\n\n    function close () {\n      emit('close')\n    }\n\n    return {\n      close,\n    }\n  },\n}\n</script>\n\n<template>\n  <transition\n    name=\"zoom\"\n    appear\n  >\n    <FocusTrap active>\n      <div\n        class=\"overlay fixed z-20 inset-0 flex flex-col items-center bg-blur p-4 sm:p-24\"\n        :class=\"{\n          'justify-start': size === 'full',\n          'justify-center': size === 'small',\n        }\"\n        @keyup.esc=\"close()\"\n      >\n        <div\n          class=\"absolute inset-0 bg-gray-900 opacity-90\"\n          @click=\"close()\"\n        />\n\n        <div\n          class=\"zoomable relative bg-gray-800 shadow-lg rounded p-4 sm:p-8 sm:pt-4 overflow-auto box\"\n          :class=\"{\n            'flex-1 w-full max-w-6xl max-h-screen': size === 'full',\n            'flex-none w-full max-w-2xl max-h-2xl': size === 'small',\n          }\"\n        >\n          <div class=\"flex items-center mb-2 sm:mb-4 lg:mb-0\">\n            <div class=\"text-gray-600 flex-1 truncate\">\n              <slot name=\"title\" />\n            </div>\n\n            <BaseButton\n              icon-left=\"close\"\n              class=\"px-4 -mr-4 py-1 text-gray-600 hover:text-gray-500\"\n              @click=\"close()\"\n            >\n              Close\n            </BaseButton>\n          </div>\n\n          <slot />\n        </div>\n      </div>\n    </FocusTrap>\n  </transition>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/RouteTab.vue",
    "content": "<script>\nexport default {\n  inheritAttrs: false,\n}\n</script>\n\n<template>\n  <BaseButton\n    v-bind=\"$attrs\"\n    class=\"tab px-4 md:px-8 pt-4 pb-3 border-transparent border-b-2 hover:bg-purple-900\"\n  >\n    <slot />\n  </BaseButton>\n</template>\n\n<style lang=\"postcss\" scoped>\n.tab.router-link-active {\n  @apply border-purple-500 rounded-b-none text-purple-300;\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/SubmitAnimation.vue",
    "content": "<template>\n  <div class=\"flex items-center justify-center\">\n    <div class=\"lds-ellipsis\">\n      <div /><div /><div /><div />\n    </div>\n  </div>\n</template>\n\n<style lang=\"postcss\" scoped>\n/* https://loading.io/css/ */\n.lds-ellipsis {\n  display: inline-block;\n  position: relative;\n  width: 64px;\n  height: 64px;\n}\n.lds-ellipsis div {\n  position: absolute;\n  top: 27px;\n  width: 11px;\n  height: 11px;\n  border-radius: 50%;\n  background: #fff;\n  animation-timing-function: cubic-bezier(0, 1, 1, 0);\n}\n.lds-ellipsis div:nth-child(1) {\n  left: 6px;\n  animation: lds-ellipsis1 0.6s infinite;\n}\n.lds-ellipsis div:nth-child(2) {\n  left: 6px;\n  animation: lds-ellipsis2 0.6s infinite;\n}\n.lds-ellipsis div:nth-child(3) {\n  left: 26px;\n  animation: lds-ellipsis2 0.6s infinite;\n}\n.lds-ellipsis div:nth-child(4) {\n  left: 45px;\n  animation: lds-ellipsis3 0.6s infinite;\n}\n@keyframes lds-ellipsis1 {\n  0% {\n    transform: scale(0);\n  }\n  100% {\n    transform: scale(1);\n  }\n}\n@keyframes lds-ellipsis3 {\n  0% {\n    transform: scale(1);\n  }\n  100% {\n    transform: scale(0);\n  }\n}\n@keyframes lds-ellipsis2 {\n  0% {\n    transform: translate(0, 0);\n  }\n  100% {\n    transform: translate(19px, 0);\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/ToastNotification.vue",
    "content": "<template>\n  <div class=\"fixed bottom-0 left-0 w-full h-0 flex items-end justify-center z-50\">\n    <div\n      class=\"w-full p-4 bg-gray-800 shadow-2xl flex flex-col items-center border-t border-gray-900\n        sm:w-auto sm:m-4 sm:rounded sm:flex-row sm:border\"\n    >\n      <div>\n        <slot />\n      </div>\n\n      <div\n        v-if=\"$slots.actions\"\n        class=\"flex mt-2 sm:mt-0 sm:ml-4\"\n      >\n        <slot name=\"actions\" />\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/about/Contributing.vue",
    "content": "<script>\nimport PageTitle from '../PageTitle.vue'\n\nexport default {\n  components: {\n    PageTitle,\n  },\n\n  metaInfo: {\n    title: 'Contributing',\n  },\n}\n</script>\n\n<template>\n  <div class=\"about-document\">\n    <div class=\"flex justify-center text-purple-500 mb-4\">\n      <i class=\"material-icons text-6xl\">thumb_up</i>\n    </div>\n\n    <PageTitle center>\n      Contributing Guide\n    </PageTitle>\n\n    <h3>Tags</h3>\n\n    <p>\n      Tags are used in filters and must adhere to the following requirements:\n    </p>\n\n    <ul class=\"list-disc my-2\">\n      <li class=\"ml-8\">\n        Tags describing the project type that the package is already attached to, for example vue, react, node.\n      </li>\n\n      <li class=\"ml-8\">\n        Useful to have not more than 3 tags on a package.\n      </li>\n\n      <li class=\"ml-8\">\n        Should not be redundant or variations of existing\n      </li>\n\n      <li class=\"ml-8\">\n        Must be brief and reuse existing as much as possible\n      </li>\n    </ul>\n\n    <h3>Category</h3>\n\n    <ul class=\"list-disc my-2\">\n      <li class=\"ml-8\">\n        Make sure you put things in the right category!\n      </li>\n    </ul>\n\n    <h3>Documentation</h3>\n\n    <ul class=\"list-disc my-2\">\n      <li class=\"ml-8\">\n        The documentation (README) contains a description of the project, illustration of the project with a demo or screenshots.\n      </li>\n\n      <li class=\"ml-8\">\n        The documentation is in English.\n      </li>\n    </ul>\n\n    <h3>Project</h3>\n\n    <ul class=\"list-disc my-2\">\n      <li class=\"ml-8\">\n        The project is active and maintained.\n      </li>\n\n      <li class=\"ml-8\">\n        Must have a link to the github repository.\n      </li>\n    </ul>\n\n    <h3>Contact Us</h3>\n\n    <p>If you have any questions about this Contributing Guide, please contact us.</p>\n\n    <p>\n      By email: guillaume@moonreach.io<br>\n      On twitter: <a\n        href=\"https://twitter.com/awesomejsapp\"\n        target=\"_blank\"\n        class=\"underline\"\n      >twitter.com/awesomejsapp</a>\n    </p>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/about/Privacy.vue",
    "content": "<script>\nimport PageTitle from '../PageTitle.vue'\n\nexport default {\n  components: {\n    PageTitle,\n  },\n\n  metaInfo: {\n    title: 'Privacy & Security',\n  },\n}\n</script>\n\n<template>\n  <div class=\"about-document\">\n    <div class=\"flex justify-center text-purple-500 mb-4\">\n      <i class=\"material-icons text-6xl\">verified_user</i>\n    </div>\n\n    <PageTitle center>\n      Privacy &amp; Security\n    </PageTitle>\n\n    <h2>🍪️ Cookies</h2>\n\n    <p>\n      Cookies necessary for the application to work may be stored on your device. By using this application you agree with the usage of those cookies.\n    </p>\n\n    <p>\n      List of the first-party 🍪️ cookies:\n    </p>\n\n    <ul class=\"list-disc my-2\">\n      <li class=\"ml-8\">\n        <span class=\"font-mono text-purple-300\">user-session</span>: used to store your authentified session allowing you to be logged in and interact with your user account.\n      </li>\n\n      <li class=\"ml-8\">\n        <span class=\"font-mono text-purple-300\">user-session-sig</span>: used to sign and verify the above <span class=\"font-mono\">user-session</span> cookie to prevent tampering.\n      </li>\n    </ul>\n\n    <h2>Privacy Policy</h2>\n\n    <p class=\"text-gray-500\">\n      Last updated: 9th October 2019\n    </p>\n\n    <p>Guillaume Chau - Akryum (\"us\", \"we\", or \"our\") operates https://awesomesj.dev (the \"Site\"). This page informs you of our policies regarding the collection, use and disclosure of Personal Information we receive from users of the Site.</p>\n\n    <p>We take Personal Information security and privacy very seriously.</p>\n\n    <p>We use your Personal Information only for providing and improving the Site. By using the Site, you agree to the collection and use of information in accordance with this policy.</p>\n\n    <h3>Information Processing Supervisor</h3>\n\n    <p>The supervisor of Information and Data Processing mentioned in this document is Mr Guillaume Chau - Akryum.</p>\n\n    <h3>Information for European Union users</h3>\n\n    <p>By using the Site and providing your information, you authorize us to collect, use, and store your information outside of the European Union.</p>\n\n    <h3>Information Collection And Use</h3>\n\n    <p>While using our Site, we may ask you to provide us with certain personally identifiable information that can be used to contact or identify you. Personally identifiable information may include, but is not limited to your names and email addresses (\"Personal Information\").</p>\n\n    <p>This Personal Information will be used to identify your content and allow other users of the Site to identify your content. Personal Information will not be used for other means than the Site functionality. Personal Information will not be sold to third-parties.</p>\n\n    <p>We don't collect any sensitive data about you. Sensitive data: racial or ethnical origin, political positions, religious or philosophical beliefs, work union membership, data related to health or sexual orientation. If any sensitive data about you would be sent to us or the Site, they will be deleted.</p>\n\n    <p>This Personal Information will be stored as long as you have a user account registered on the Site. You can ask us anytime to delete your user account and all associated Personal Information by contacting us (see \"Contact Us\" section below).</p>\n\n    <h3>Communications</h3>\n\n    <p>We may use your Personal Information to contact you with newsletters, marketing or promotional materials and other information that you opted to, like notifications and other transactional emails. You can unsubscribe anytime from our communications by clicking in the \"Unsubscribe\" link in the emails or by contacting us (see \"Contact Us\" section below).</p>\n\n    <h3>Cookies</h3>\n\n    <p>Cookies are files with small amount of data, which may include an anonymous unique identifier. Cookies are sent to your browser from a web site and stored on your computer's hard drive.</p>\n\n    <p>Like many sites, we use \"cookies\" to collect information. You can instruct your browser to refuse all cookies or to indicate when a cookie is being sent. However, if you do not accept cookies, you may not be able to use some portions of our Site.</p>\n\n    <h3>Security</h3>\n\n    <p>The security of your Personal Information is important to us, but remember that no method of transmission over the Internet, or method of electronic storage, is 100% secure. While we strive to use commercially acceptable means to protect your Personal Information, we cannot guarantee its absolute security.</p>\n\n    <h3>Hosting</h3>\n\n    <p>All data processed and stored by the Site is hosted on Amazon Web Services and Google Cloud. They are provided by intermediaries such as Zeit Now and FaunaDB. The hosting locations may vary and be worldwide, in order to provide the best performance and ensure the Site functionality is best quality possible. Hosting data processed by the Site physically closer to you improves the Site performance for your experience.</p>\n\n    <h3>Changes To This Privacy Policy</h3>\n\n    <p>This Privacy Policy is effective as of 8th October 2019 and will remain in effect except with respect to any changes in its provisions in the future, which will be in effect immediately after being posted on this page.</p>\n\n    <p>We reserve the right to update or change our Privacy Policy at any time and you should check this Privacy Policy periodically. Your continued use of the Service after we post any modifications to the Privacy Policy on this page will constitute your acknowledgment of the modifications and your consent to abide and be bound by the modified Privacy Policy.</p>\n\n    <p>If we make any material changes to this Privacy Policy, we will notify you either through the email address you have provided us, or by placing a prominent notice on our website.</p>\n\n    <h3>Contact Us</h3>\n\n    <p>If you have any questions about this Privacy Policy, please contact us.</p>\n\n    <p>\n      By email: guillaume@moonreach.io<br>\n      On twitter: <a\n        href=\"https://twitter.com/awesomejsapp\"\n        target=\"_blank\"\n        class=\"underline\"\n      >twitter.com/awesomejsapp</a>\n    </p>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/admin/AdminDashboard.vue",
    "content": "<script>\nimport { useCurrentUser } from '../user/useCurrentUser'\n\nimport ErrorMessage from '../ErrorMessage.vue'\nimport PageTitle from '../PageTitle.vue'\nimport RouteTab from '../RouteTab.vue'\nimport UserCheckSignedIn from '../user/UserCheckSignedIn.vue'\n\nexport default {\n  components: {\n    ErrorMessage,\n    PageTitle,\n    RouteTab,\n    UserCheckSignedIn,\n  },\n\n  setup () {\n    return {\n      isAdmin: useCurrentUser().isAdmin,\n    }\n  },\n}\n</script>\n\n<template>\n  <div>\n    <UserCheckSignedIn />\n\n    <ErrorMessage\n      v-if=\"!isAdmin\"\n      error=\"Restricted area\"\n      class=\"error-box\"\n    />\n\n    <template v-else>\n      <PageTitle class=\"justify-center mb-4 lg:pl-4\">\n        <i class=\"material-icons text-3xl mr-2 text-red-700\">lock</i>\n        <span>Admin Restricted Area</span>\n      </PageTitle>\n\n      <div class=\"overflow-x-auto flex pb-4 lg:pb-0\">\n        <RouteTab\n          :to=\"{ name: 'admin' }\"\n          icon-left=\"home\"\n          class=\"flex-none\"\n          exact\n        >\n          Home\n        </RouteTab>\n\n        <RouteTab\n          :to=\"{ name: 'admin-teams' }\"\n          icon-left=\"supervisor_account\"\n          class=\"flex-none\"\n        >\n          Teams\n        </RouteTab>\n      </div>\n\n      <router-view />\n    </template>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/admin/AdminHome.vue",
    "content": "<template>\n  <div>\n    <div class=\"mt-16 flex flex-col items-center text-gray-600\">\n      <i class=\"material-icons text-5xl\">build</i>\n\n      <span class=\"text-xl mt-4\">Work in progress</span>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/admin/AdminTeamCreate.vue",
    "content": "<script>\nimport { ref } from '@vue/composition-api'\nimport { useMutation } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\nimport { teamFragment } from './fragments'\nimport { useRouter } from '@/util/router'\n\nimport AdminTeamEditForm from './AdminTeamEditForm.vue'\nimport { QUERY_TEAMS } from './AdminTeams.vue'\n\nexport default {\n  components: {\n    AdminTeamEditForm,\n  },\n\n  setup (props) {\n    const team = ref({\n      name: '',\n      projectTypes: [],\n      users: [],\n    })\n\n    const { mutate, loading: submitting, error, onDone } = useMutation(gql`\n      mutation CreateTeam ($input: CreateTeamInput!) {\n        createTeam (input: $input) {\n          ...team\n        }\n      }\n      ${teamFragment}\n    `, {\n      update: (cache, { data: { createTeam } }) => {\n        const data = cache.readQuery({ query: QUERY_TEAMS })\n        data.allTeams.push(createTeam)\n        cache.writeQuery({ query: QUERY_TEAMS, data })\n      },\n    })\n\n    // Redirect after create\n    const router = useRouter()\n    onDone((result) => {\n      router.push({\n        name: 'admin-team-view',\n        params: {\n          teamId: result.data.createTeam.id,\n        },\n      })\n    })\n\n    async function createTeam (data) {\n      await mutate({\n        input: {\n          ...data,\n        },\n      })\n    }\n\n    return {\n      team,\n      createTeam,\n      submitting,\n      error,\n    }\n  },\n}\n</script>\n\n<template>\n  <div\n    class=\"pb-32\"\n  >\n    <h2 class=\"text-xl text-gray-600 mb-8\">\n      Create team\n    </h2>\n\n    <AdminTeamEditForm\n      :team=\"team\"\n      :submitting=\"submitting\"\n      :error=\"error\"\n      @submit=\"createTeam\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/admin/AdminTeamEditForm.vue",
    "content": "<script>\nimport { ref, watch } from '@vue/composition-api'\n\nimport ErrorMessage from '../ErrorMessage.vue'\nimport ProjectTypeMultiSelect from '../project-type/ProjectTypeMultiSelect.vue'\nimport UserMultiSelect from './UserMultiSelect.vue'\n\nexport default {\n  components: {\n    ErrorMessage,\n    ProjectTypeMultiSelect,\n    UserMultiSelect,\n  },\n\n  props: {\n    team: {\n      type: Object,\n      required: true,\n    },\n\n    submitting: {\n      type: Boolean,\n      default: false,\n    },\n\n    error: {\n      type: [String, Object, Error],\n      default: null,\n    },\n  },\n\n  setup (props, { emit }) {\n    const formData = ref()\n\n    function reset () {\n      formData.value = {\n        name: props.team.name,\n        projectTypeIds: props.team.projectTypes.map(pt => pt.id),\n        userIds: props.team.users.map(u => u.id),\n      }\n    }\n    reset()\n    // Auto reset after submit\n    watch(() => props.submitting, value => {\n      if (!value && !props.error) {\n        reset()\n      }\n    }, {\n      lazy: true,\n    })\n    watch(() => props.team, () => reset())\n\n    function submit () {\n      const result = {\n        ...formData.value,\n      }\n      emit('submit', result)\n    }\n\n    return {\n      formData,\n      submit,\n    }\n  },\n}\n</script>\n\n<template>\n  <form\n    class=\"max-w-3xl\"\n    @submit.prevent=\"submit()\"\n  >\n    <div class=\"lg:flex items-baseline mt-4\">\n      <label\n        for=\"team-name\"\n        class=\"flex-none lg:mr-8 lg:w-40 text-gray-500\"\n      >\n        Name:\n      </label>\n\n      <input\n        id=\"team-name\"\n        v-model=\"formData.name\"\n        placeholder=\"Team name\"\n        maxlength=\"200\"\n        class=\"mt-2 lg:mt-0 bg-black px-8 py-4 rounded w-full\"\n      >\n    </div>\n\n    <div class=\"lg:flex items-center mt-8\">\n      <label\n        for=\"projectTypes\"\n        class=\"flex-none lg:mr-8 lg:w-40 text-gray-500\"\n      >\n        Project types:\n      </label>\n\n      <ProjectTypeMultiSelect\n        id=\"projectTypes\"\n        v-model=\"formData.projectTypeIds\"\n      />\n    </div>\n\n    <div class=\"lg:flex items-center mt-8\">\n      <label\n        for=\"users\"\n        class=\"flex-none lg:mr-8 lg:w-40 text-gray-500\"\n      >\n        Users:\n      </label>\n\n      <UserMultiSelect\n        id=\"users\"\n        v-model=\"formData.userIds\"\n      />\n    </div>\n\n    <div class=\"mt-8 flex items-center justify-end\">\n      <slot name=\"actions\" />\n\n      <BaseButton\n        :loading=\"submitting\"\n        type=\"submit\"\n        icon-left=\"save\"\n        class=\"bg-purple-800 hover:bg-purple-700 px-8 py-4\"\n      >\n        Save team\n      </BaseButton>\n    </div>\n\n    <ErrorMessage\n      :error=\"error\"\n      class=\"error-box mt-8\"\n    />\n  </form>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/admin/AdminTeamView.vue",
    "content": "<script>\nimport { useQuery, useResult, useMutation } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\n\nimport AdminTeamEditForm from './AdminTeamEditForm.vue'\nimport LoadingIndicator from '../LoadingIndicator.vue'\n\nconst teamFragment = gql`\nfragment team on Team {\n  id\n  name\n  projectTypes {\n    id\n    name\n  }\n  users {\n    id\n    nickname\n    email\n  }\n}\n`\n\nexport default {\n  components: {\n    AdminTeamEditForm,\n    LoadingIndicator,\n  },\n\n  props: {\n    teamId: {\n      type: String,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { result, loading } = useQuery(gql`\n      query GetTeam ($id: ID!) {\n        team (id: $id) {\n          ...team\n        }\n      }\n      ${teamFragment}\n    `, () => ({\n      id: props.teamId,\n    }))\n    const team = useResult(result)\n\n    const { mutate, loading: submitting, error } = useMutation(gql`\n      mutation EditTeam ($input: EditTeamInput!) {\n        editTeam (input: $input) {\n          ...team\n        }\n      }\n      ${teamFragment}\n    `)\n\n    async function editTeam (data) {\n      await mutate({\n        input: {\n          id: props.teamId,\n          ...data,\n        },\n      })\n    }\n\n    return {\n      team,\n      loading,\n\n      editTeam,\n      submitting,\n      error,\n    }\n  },\n}\n</script>\n\n<template>\n  <LoadingIndicator v-if=\"loading\" />\n\n  <div\n    v-else\n    class=\"pb-32\"\n  >\n    <h2 class=\"text-xl text-gray-600 mb-8\">\n      Selected team\n    </h2>\n\n    <AdminTeamEditForm\n      :team=\"team\"\n      :submitting=\"submitting\"\n      :error=\"error\"\n      @submit=\"editTeam\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/admin/AdminTeams.vue",
    "content": "<script>\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\nimport { teamFragment } from './fragments'\n\nimport LoadingIndicator from '../LoadingIndicator.vue'\nimport { computed } from '@vue/composition-api'\n\nexport const QUERY_TEAMS = gql`\n  query AllTeams {\n    allTeams {\n      ...team\n    }\n  }\n  ${teamFragment}\n`\n\nexport default {\n  components: {\n    LoadingIndicator,\n  },\n\n  setup (props, { root }) {\n    const { result, loading } = useQuery(QUERY_TEAMS)\n    const teams = useResult(result, [])\n\n    const isRootRoute = computed(() => root.$route.name === 'admin-teams')\n\n    return {\n      teams,\n      loading,\n      isRootRoute,\n    }\n  },\n}\n</script>\n\n<template>\n  <div class=\"flex mt-4\">\n    <div\n      v-if=\"!$responsive.lg || isRootRoute\"\n      class=\"w-full lg:w-1/3 lg:pb-64 lg:mt-4 lg:sticky lg:top-4 lg:max-h-screen lg:overflow-y-auto\"\n      :class=\"{\n        'scroll-parent': !$responsive.lg,\n      }\"\n    >\n      <BaseButton\n        :to=\"{ name: 'admin-team-create' }\"\n        icon-left=\"add\"\n        align=\"left\"\n        class=\"link w-full bg-gray-800 text-purple-300 rounded px-6 py-4 hover:bg-gray-700\"\n      >\n        Create team\n      </BaseButton>\n\n      <LoadingIndicator v-if=\"loading\" />\n\n      <template v-else>\n        <BaseButton\n          v-for=\"team of teams\"\n          :key=\"team.id\"\n          :to=\"{\n            name: 'admin-team-view',\n            params: {\n              teamId: team.id,\n            },\n          }\"\n          icon-left=\"supervisor_account\"\n          align=\"left\"\n          class=\"link w-full mt-4 sm:mt-6 bg-gray-800 text-purple-300 rounded px-6 py-4 hover:bg-gray-700\"\n        >\n          {{ team.name }}\n        </BaseButton>\n      </template>\n    </div>\n\n    <div\n      v-if=\"!$responsive.lg || !isRootRoute\"\n      class=\"w-full lg:w-2/3 lg:pl-16\"\n    >\n      <router-view />\n    </div>\n  </div>\n</template>\n\n<style lang=\"postcss\" scoped>\n.link.router-link-active {\n  @apply text-purple-200 bg-purple-800;\n\n  &:hover {\n    @apply bg-purple-700;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/admin/UserMultiSelect.vue",
    "content": "<script>\nimport MultiSelect from 'vue-multiselect'\nimport gql from 'graphql-tag'\nimport { computed } from '@vue/composition-api'\nimport { useQuery, useResult } from '@vue/apollo-composable'\n\nexport default {\n  components: {\n    MultiSelect,\n  },\n\n  model: {\n    prop: 'userIds',\n    event: 'update',\n  },\n\n  props: {\n    userIds: {\n      type: Array,\n      required: true,\n    },\n  },\n\n  setup (props, { emit }) {\n    // Project types\n    const { result } = useQuery(gql`\n      query ProjectTypes {\n        allUsers {\n          id\n          nickname\n          email\n        }\n      }\n    `)\n    const users = useResult(result, [])\n\n    const model = computed({\n      get: () => users.value.filter(u => props.userIds.includes(u.id)),\n      set: (value) => {\n        emit('update', value.map(u => u.id))\n      },\n    })\n\n    return {\n      users,\n      model,\n    }\n  },\n}\n</script>\n\n<template>\n  <MultiSelect\n    v-model=\"model\"\n    :options=\"users\"\n    :close-on-select=\"false\"\n    label=\"nickname\"\n    track-by=\"id\"\n    placeholder=\"Select users...\"\n    multiple\n  >\n    <template #option=\"{ option }\">\n      <div class=\"flex items-baseline\">\n        <div class=\"mr-4\">\n          {{ option.nickname }}\n        </div>\n        <div class=\"text-gray-500\">\n          {{ option.email }}\n        </div>\n      </div>\n    </template>\n  </MultiSelect>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/admin/fragments.js",
    "content": "import gql from 'graphql-tag'\n\nexport const teamFragment = gql`\nfragment team on Team {\n  id\n  name\n}\n`\n"
  },
  {
    "path": "packages/frontend/src/components/app/AppFooter.vue",
    "content": "<script>\nimport AppSponsors from './AppSponsors.vue'\n\nexport default {\n  components: {\n    AppSponsors,\n  },\n}\n</script>\n\n<template>\n  <footer\n    class=\"text-center text-gray-700 px-8 py-16\"\n  >\n    <AppSponsors />\n\n    <div class=\"space-x-4\">\n      <span class=\"inline-block\">\n        <router-link\n          :to=\"{ name: 'about-privacy' }\"\n          class=\"underline\"\n        >Privacy &amp; Cookies</router-link>\n      </span>\n\n      <span class=\"inline-block\">\n        <router-link\n          :to=\"{ name: 'about-contributing' }\"\n          class=\"underline\"\n        >Contributing</router-link>\n      </span>\n\n      <span class=\"inline-block\">\n        Follow us on\n        <a\n          href=\"https://twitter.com/awesomejsapp\"\n          target=\"_blank\"\n          class=\"underline\"\n        >Twitter</a>\n      </span>\n\n      <span class=\"inline-block\">\n        Fork on\n        <a\n          href=\"https://github.com/Akryum/awesomejs.dev\"\n          target=\"_blank\"\n          class=\"underline\"\n        >Github</a>\n      </span>\n\n      <span class=\"inline-block\">\n        Some icons from\n        <a\n          href=\"https://icons8.com\"\n          target=\"_blank\"\n          class=\"underline\"\n        >icons8</a>\n      </span>\n    </div>\n  </footer>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/app/AppGlobalLoading.vue",
    "content": "<script>\nimport { watch, ref } from '@vue/composition-api'\nimport { useGlobalQueryLoading } from '@vue/apollo-composable'\n\nexport default {\n  setup () {\n    const loading = useGlobalQueryLoading()\n    const progress = ref(0)\n\n    watch(loading, (value, oldValue, onCleanup) => {\n      if (value) {\n        progress.value = 0\n        const timer = setInterval(() => {\n          progress.value += (1 - progress.value) / 8\n        }, 200)\n        onCleanup(() => {\n          clearInterval(timer)\n        })\n      } else {\n        progress.value = 1\n      }\n    })\n\n    return {\n      progress,\n    }\n  },\n}\n</script>\n\n<template>\n  <div\n    class=\"container fixed top-0 left-0 w-full h-0\"\n    :class=\"{\n      'opacity-0': progress === 1,\n    }\"\n  >\n    <div\n      class=\"bar bg-purple-600\"\n      :class=\"{\n        animate: progress > 0,\n      }\"\n      :style=\"{\n        width: `${progress * 100}%`\n      }\"\n    />\n  </div>\n</template>\n\n<style lang=\"postcss\" scoped>\n.container {\n  transition: opacity .5s ease-out;\n}\n\n.bar {\n  height: 2px;\n  box-shadow: 0 2px 6px theme('colors.purple.300');\n\n  &.animate {\n    transition: width .2s ease-out;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/app/AppHeader.vue",
    "content": "<script>\nimport { ref } from '@vue/composition-api'\nimport { isMac } from '@/util/env'\nimport AppHeaderLogo from './AppHeaderLogo.vue'\nimport PackageAddButton from '../pkg/PackageAddButton.vue'\nimport UserMenu from '../user/UserMenu.vue'\nimport SearchOverlayAsyncState from '../search/SearchOverlayAsyncState.vue'\nconst SearchOverlayLoading = {\n  render: h => h(SearchOverlayAsyncState, { props: { state: 'loading' } }),\n}\nconst SearchOverlayError = {\n  render (h) {\n    return h(SearchOverlayAsyncState, { props: { state: 'error' }, on: this.$listeners })\n  },\n}\nconst SearchOverlay = () => ({\n  component: import(\n    /* webpackChunkName: \"SearchOverlay.vue\" */\n    '../search/SearchOverlay.vue'\n  ),\n  // A component to use while the async component is loading\n  loading: SearchOverlayLoading,\n  // A component to use if the load fails\n  error: SearchOverlayError,\n  // Delay before showing the loading component. Default: 200ms.\n  delay: 200,\n  // The error component will be displayed if a timeout is\n  // provided and exceeded. Default: Infinity.\n  timeout: 3000,\n})\n\nexport default {\n  components: {\n    AppHeaderLogo,\n    PackageAddButton,\n    SearchOverlay,\n    UserMenu,\n  },\n\n  setup () {\n    const openSearch = ref(false)\n    return {\n      openSearch,\n      isMac,\n    }\n  },\n}\n</script>\n\n<template>\n  <header>\n    <div class=\"flex items-center px-4 mt-4 lg:px-16 lg:my-8 relative\">\n      <AppHeaderLogo class=\"flex-none\" />\n\n      <div class=\"flex-1\" />\n\n      <PackageAddButton\n        class=\"mr-4\"\n      />\n      <UserMenu />\n\n      <div\n        v-if=\"!$responsive.sm\"\n        class=\"absolute top-0 left-0 w-full h-0 flex justify-center\"\n      >\n        <div>\n          <BaseButton\n            v-tooltip=\"`Search packages <span class='text-gray-600 font-mono'>${isMac ? '⌘' : 'Ctrl'}+F</span>`\"\n            icon-left=\"search\"\n            class=\"px-4 py-2 text-gray-500 hover:text-gray-400 hover:bg-gray-800\"\n            @click=\"openSearch = true\"\n          >\n            Search...\n          </BaseButton>\n        </div>\n      </div>\n    </div>\n\n    <!-- FAB -->\n    <div\n      v-if=\"$responsive.sm\"\n      class=\"fixed bottom-0 right-0 p-8 z-20\"\n    >\n      <BaseButton\n        class=\"p-3 bg-yellow-500 rounded-full shadow-xl text-black\"\n        @click=\"openSearch = true\"\n      >\n        <i class=\"material-icons\">search</i>\n      </BaseButton>\n    </div>\n\n    <transition name=\"fade\">\n      <keep-alive include=\"SearchOverlay\">\n        <SearchOverlay\n          v-if=\"openSearch\"\n          @close=\"openSearch = false\"\n        />\n      </keep-alive>\n    </transition>\n\n    <GlobalEvents\n      @keydown.ctrl.70.prevent=\"openSearch = !openSearch\"\n    />\n  </header>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/app/AppHeaderLogo.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { computed, ref } from '@vue/composition-api'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { getNamedParents, useRouter, useRoute } from '@/util/router'\nimport { projectTypeFragment } from '../project-type/fragments'\n\nexport default {\n  setup (props, { root }) {\n    const currentRoute = useRoute()\n    const hasProjectType = computed(() => !!currentRoute.value.params.projectTypeSlug)\n\n    const { result } = useQuery(gql`\n      query ProjectType ($slug: String!) {\n        projectType: projectTypeBySlug (slug: $slug) {\n          ...projectType\n        }\n      }\n      ${projectTypeFragment}\n    `, () => ({\n      slug: currentRoute.value.params.projectTypeSlug,\n    }), () => ({\n      enabled: !!hasProjectType.value,\n    }))\n    const projectType = useResult(result)\n\n    const src = computed(() => {\n      if (hasProjectType.value) {\n        return projectType.value && projectType.value.logo\n      }\n      return require('@/assets/logo.png')\n    })\n\n    const router = useRouter()\n    const route = computed(() => {\n      if (root.$responsive.lg) {\n        const parents = getNamedParents(router.options.routes, currentRoute.value.matched)\n        if (parents.length) {\n          return {\n            name: parents[parents.length - 1].name,\n          }\n        }\n      }\n      return { name: 'home' }\n    })\n\n    // Back button\n    const hover = ref(false)\n    const showBack = computed(() => hover.value && currentRoute.value.matched.length > 1)\n\n    return {\n      route,\n      src,\n      hover,\n      showBack,\n    }\n  },\n}\n</script>\n\n<template>\n  <BaseButton\n    class=\"link\"\n    :to=\"route\"\n    @mouseenter.native=\"hover = true\"\n    @mouseleave.native=\"hover = false\"\n  >\n    <transition-group\n      name=\"fade\"\n      class=\"relative w-8 h-8\"\n      tag=\"div\"\n    >\n      <img\n        v-if=\"src\"\n        v-show=\"!showBack\"\n        key=\"image\"\n        :src=\"src\"\n        class=\"w-full h-full rounded\"\n        alt=\"Logo\"\n      >\n      <div\n        v-show=\"showBack\"\n        key=\"arrow\"\n        class=\"absolute inset-0 text-yellow-900 bg-yellow-400 rounded-full flex items-center justify-center\"\n      >\n        <i class=\"material-icons text-xl\">home</i>\n      </div>\n    </transition-group>\n  </BaseButton>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/app/AppHome.vue",
    "content": "<script>\nimport PageTitle from '../PageTitle.vue'\nimport ProjectTypes from '../project-type/ProjectTypes.vue'\nimport { STORE_ROUTE_BEFORE_REDIRECT, useRouter } from '@/util/router'\n\nexport default {\n  components: {\n    PageTitle,\n    ProjectTypes,\n  },\n\n  setup (props) {\n    const router = useRouter()\n    const routeBeforeRedirect = localStorage.getItem(STORE_ROUTE_BEFORE_REDIRECT)\n    if (routeBeforeRedirect) {\n      localStorage.removeItem(STORE_ROUTE_BEFORE_REDIRECT)\n      router.push(routeBeforeRedirect)\n    }\n  },\n}\n</script>\n\n<template>\n  <div>\n    <PageTitle>\n      Select your project type\n    </PageTitle>\n\n    <ProjectTypes />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/app/AppServiceWorkerManager.vue",
    "content": "<script>\nimport { useAppUpdate } from '@/util/service-worker'\nconst ToastNotification = () => import(\n  /* webpackChunkName: \"ToastNotification.vue\" */\n  '../ToastNotification.vue'\n)\n\nexport default {\n  components: {\n    ToastNotification,\n  },\n\n  setup () {\n    const {\n      updateAvailable,\n      applyUpdate,\n    } = useAppUpdate()\n\n    return {\n      updateAvailable,\n      applyUpdate,\n    }\n  },\n}\n</script>\n\n<template>\n  <ToastNotification\n    v-if=\"updateAvailable\"\n  >\n    <i class=\"material-icons text-xl\">cached</i>\n    A new version of the application is available.\n\n    <template #actions>\n      <BaseButton\n        class=\"px-4 py-2 hover:bg-gray-700\"\n        @click=\"applyUpdate\"\n      >\n        Refresh\n      </BaseButton>\n\n      <BaseButton\n        class=\"px-4 py-2 hover:bg-gray-700\"\n        @click=\"updateAvailable = false\"\n      >\n        Dismiss\n      </BaseButton>\n    </template>\n  </ToastNotification>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/app/AppSponsors.vue",
    "content": "<template>\n  <div class=\"space-y-4 p-4\">\n    <div>Special thanks to our Sponsors:</div>\n    <div class=\"flex items-center justify-center space-x-8\">\n      <a\n        href=\"https://algolia.com\"\n        target=\"_blank\"\n      >\n        <img\n          src=\"~@/assets/search-by-algolia-dark-background.svg\"\n          alt=\"Search by Algolia\"\n          class=\"max-h-6\"\n        >\n      </a>\n      <a\n        href=\"https://fauna.com\"\n        target=\"_blank\"\n      >\n        <img\n          src=\"~@/assets/fauna.svg\"\n          alt=\"Fauna\"\n          class=\"max-h-6\"\n        >\n      </a>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/chart/DotChart.vue",
    "content": "<script>\nimport { scaleLinear, extent } from 'd3'\nimport { computed, watch, ref } from '@vue/composition-api'\n\nimport LoadingIndicator from '../LoadingIndicator.vue'\n\nexport default {\n  components: {\n    LoadingIndicator,\n  },\n\n  props: {\n    points: {\n      type: Array,\n      required: true,\n    },\n\n    loading: {\n      type: Boolean,\n      default: false,\n    },\n\n    title: {\n      type: String,\n      required: true,\n    },\n\n    icon: {\n      type: String,\n      default: null,\n    },\n  },\n\n  setup (props) {\n    const values = computed(() => props.points.map(pt => pt.value))\n    const scale = computed(() => scaleLinear()\n      // @ts-ignore\n      .domain(extent(values.value))\n      .range([0, 100]),\n    )\n\n    const graph = ref(null)\n\n    // Auto scroll\n    function scrollToRight () {\n      if (graph.value) {\n        graph.value.scrollLeft = graph.value.scrollWidth\n      }\n    }\n    watch(values, () => {\n      scrollToRight()\n    })\n    watch(graph, () => {\n      scrollToRight()\n    })\n\n    return {\n      scale,\n      graph,\n    }\n  },\n}\n</script>\n\n<template>\n  <div class=\"border-blue-900 border-2 rounded p-4 mb-4\">\n    <!-- Header -->\n    <div>\n      <h3 class=\"text-blue-400\">\n        <i\n          v-if=\"icon\"\n          class=\"material-icons text-xl mr-2 text-blue-600\"\n        >{{ icon }}</i>\n        {{ title }}\n      </h3>\n    </div>\n\n    <div\n      v-if=\"loading\"\n      class=\"pt-8 pb-4\"\n    >\n      <LoadingIndicator\n        class=\"h-32 text-blue-500\"\n      />\n      <div class=\"h-5\" />\n    </div>\n\n    <!-- Graph -->\n    <div\n      v-else\n      ref=\"graph\"\n      class=\"flex overflow-x-auto\"\n    >\n      <div\n        v-for=\"(point, index) of points\"\n        :key=\"index\"\n        v-tooltip=\"{ content: point.tooltip, placement: 'bottom', delay: { show: 0, hide: 0 } }\"\n        class=\"group flex-1 pt-8\"\n      >\n        <div class=\"w-2 px-2 h-32 relative\">\n          <div\n            class=\"absolute bottom-0 w-2 rounded bg-blue-800 group-hover:bg-blue-700\"\n            :style=\"{\n              height: `calc(${scale(point.value)}% + 4px)`,\n            }\"\n          />\n          <div\n            class=\"absolute w-2 h-2 rounded-full bg-blue-500 group-hover:bg-blue-400\"\n            :style=\"{\n              bottom: `${scale(point.value)}%`,\n            }\"\n          />\n        </div>\n\n        <!-- Label -->\n        <div\n          v-if=\"(index - 2) % 5 === 0\"\n          class=\"w-2 px-2 h-5 mt-4\"\n        >\n          <div class=\"relative -ml-8 w-16 text-center text-xs text-gray-600 group-hover:text-gray-500\">\n            {{ point.label }}\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/NoPackageSelected.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { projectTypeFragment } from '../project-type/fragments'\nimport { useQuery, useResult } from '@vue/apollo-composable'\n\nexport default {\n  setup (props, { root }) {\n    const { result } = useQuery(gql`\n      query ProjectType ($slug: String!) {\n        projectType: projectTypeBySlug (slug: $slug) {\n          ...projectType\n        }\n      }\n      ${projectTypeFragment}\n    `, () => ({\n      slug: root.$route.params.projectTypeSlug,\n    }))\n    const projectType = useResult(result)\n\n    return {\n      projectType,\n    }\n  },\n}\n</script>\n\n<template>\n  <div class=\"text-center my-8\">\n    <div\n      v-if=\"projectType\"\n      class=\"logo mx-auto mb-6 flex items-center justify-center\"\n    >\n      <img\n        :src=\"projectType.logo\"\n        :alt=\"`${projectType.name} logo`\"\n        class=\"max-w-full max-h-full rounded\"\n      >\n    </div>\n\n    <h1 class=\"text-xl font-light text-gray-600\">\n      <i class=\"material-icons mr-2 text-gray-700\">arrow_back</i>\n      <span>Select a package to continue</span>\n    </h1>\n  </div>\n</template>\n\n<style lang=\"postcss\" scoped>\n.logo {\n  width: 100px;\n  height: 100px;\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageAddButton.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { computed } from '@vue/composition-api'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { isMac } from '@/util/env'\nimport { projectTypeFragment } from '../project-type/fragments'\n\nexport default {\n  setup (props, { root }) {\n    const projectTypeSlug = computed(() => root.$route.params.projectTypeSlug)\n\n    const { result } = useQuery(gql`\n      query ProjectType ($slug: String!) {\n        projectType: projectTypeBySlug (slug: $slug) {\n          ...projectType\n        }\n      }\n      ${projectTypeFragment}\n    `, () => ({\n      slug: projectTypeSlug.value,\n    }), () => ({\n      enabled: !!projectTypeSlug.value,\n    }))\n    const projectType = useResult(result)\n\n    const route = computed(() => ({\n      name: 'add-package',\n      query: {\n        projectTypeId: projectType.value ? projectType.value.id : null,\n      },\n    }))\n\n    return {\n      route,\n      isMac,\n    }\n  },\n}\n</script>\n\n<template>\n  <BaseButton\n    v-tooltip=\"`Suggest a new package <span class='text-gray-600 font-mono'>${isMac ? '⌘' : 'Ctrl'}+Shift+A</span>`\"\n    :to=\"route\"\n    icon-left=\"add\"\n    class=\"px-4 py-2 bg-purple-900 text-purple-200 hover:bg-purple-800\"\n  >\n    Package\n\n    <GlobalEvents\n      @keydown.ctrl.shift.65.prevent=\"$router.push(route)\"\n    />\n  </BaseButton>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageAddWizard.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { ref, reactive, computed, watch } from '@vue/composition-api'\nimport { useQuery, useResult, useMutation } from '@vue/apollo-composable'\nimport { pkgFragment, pkgProposalFragment } from './fragments'\nimport { useNpmSearch } from '@/util/algolia-npm'\nimport { useAvailableTags } from '@/util/tags'\n\nimport ErrorMessage from '../ErrorMessage.vue'\nimport MultiSelect from 'vue-multiselect'\nimport PackageAdded from './PackageAdded.vue'\nimport PackageListItem from './PackageListItem.vue'\nimport PageTitle from '../PageTitle.vue'\nimport ProjectTypeSelect from '../project-type/ProjectTypeSelect.vue'\nimport UserCheckSignedIn from '../user/UserCheckSignedIn.vue'\n\nexport default {\n  components: {\n    ErrorMessage,\n    MultiSelect,\n    PackageAdded,\n    PackageListItem,\n    PageTitle,\n    ProjectTypeSelect,\n    UserCheckSignedIn,\n  },\n\n  setup (props, { root }) {\n    // Form data\n    const projectTypeId = ref(root.$route.query.projectTypeId || null)\n    const formData = reactive({\n      packageName: root.$route.query.packageName || '',\n      tags: [],\n    })\n\n    watch(() => root.$route, value => {\n      projectTypeId.value = value.query.projectTypeId || null\n      formData.packageName = value.query.packageName || ''\n    })\n\n    // Check for existing proposals & packages\n    const { result, loading } = useQuery(gql`\n      query PackageProposalAndPackageByName ($name: String!) {\n        proposal: packageProposalByName (name: $name) {\n          ...pkgProposal\n          projectTypes {\n            id\n            name\n            slug\n          }\n        }\n\n        pkg: packageByName (name: $name) {\n          ...pkg\n          projectTypes {\n            id\n            name\n            slug\n          }\n        }\n      }\n      ${pkgFragment}\n      ${pkgProposalFragment}\n    `, () => ({\n      name: formData.packageName,\n    }), () => ({\n      enabled: !!formData.packageName,\n      debounce: 1000,\n    }))\n    const proposal = useResult(result, null, data => data.proposal)\n    const pkg = useResult(result, null, data => data.pkg)\n    const alreadyProposed = computed(() => formData.packageName && !loading.value && proposal.value)\n    const alreadyExists = computed(() => formData.packageName && !loading.value && pkg.value)\n\n    // Form validation\n    const requiredFieldsValid = computed(() => projectTypeId.value != null && !!formData.packageName)\n    const valid = computed(() => requiredFieldsValid.value && !alreadyProposed.value && !alreadyExists.value)\n\n    // Added summary\n    const added = ref(false)\n    const addedProposal = ref(null)\n\n    // Submit\n\n    const { mutate, error, loading: submitting, onDone } = useMutation(gql`\n      mutation ProposePackage ($input: ProposePackageInput!) {\n        proposePackage (input: $input) {\n          ...pkgProposal\n          projectTypes {\n            id\n            name\n            slug\n          }\n        }\n      }\n      ${pkgProposalFragment}\n    `)\n\n    async function submit () {\n      if (!valid.value) {\n        if (!requiredFieldsValid.value) {\n          error.value = `Project type and package name are required`\n        }\n        return\n      }\n      await mutate({\n        input: {\n          projectTypeId: projectTypeId.value,\n          packageName: formData.packageName,\n          tags: formData.tags,\n        },\n      })\n    }\n\n    onDone(({ data }) => {\n      addedProposal.value = data.proposePackage\n      added.value = true\n    })\n\n    function addAnother () {\n      formData.packageName = ''\n      formData.tags = []\n      added.value = false\n    }\n\n    // NPM search\n    const { searchText: npmSearchText, result: npmSearchResult } = useNpmSearch({\n      hitsPerPage: 5,\n    })\n\n    watch(() => formData.packageName, value => {\n      npmSearchText.value = value\n    })\n\n    function selectNpmSuggestion (result) {\n      formData.packageName = result.name\n      formData.tags = result.keywords\n    }\n\n    // Available tags\n    const { availableTags } = useAvailableTags(projectTypeId, () => formData.tags)\n\n    return {\n      projectTypeId,\n      formData,\n\n      alreadyProposed,\n      proposal,\n      alreadyExists,\n      pkg,\n\n      submit,\n      submitting,\n      error,\n\n      added,\n      addedProposal,\n      addAnother,\n\n      npmSearchResult,\n      selectNpmSuggestion,\n\n      availableTags,\n    }\n  },\n}\n</script>\n\n<template>\n  <div v-if=\"!added\">\n    <UserCheckSignedIn />\n\n    <PageTitle>\n      Add a package\n    </PageTitle>\n\n    <div class=\"mt-4 text-gray-600\">\n      <i class=\"material-icons text-lg mr-2\">thumb_up</i>\n      Contribute to the awesomeness by proposing a package!\n    </div>\n\n    <div class=\"mt-8 max-w-lg\">\n      <ProjectTypeSelect\n        v-model=\"projectTypeId\"\n      />\n\n      <input\n        v-model=\"formData.packageName\"\n        v-focus.lazy=\"true\"\n        placeholder=\"Enter package name on npm\"\n        maxlength=\"80\"\n        class=\"mt-8 bg-black px-8 py-4 rounded w-full\"\n      >\n\n      <div\n        v-if=\"alreadyProposed || alreadyExists\"\n        class=\"text-orange-500 mt-4\"\n      >\n        <i class=\"material-icons text-lg mr-2\">error</i>\n        <span v-if=\"alreadyExists\">This package already exists in the app:</span>\n        <span v-if=\"alreadyProposed\">This package is already proposed:</span>\n      </div>\n\n      <PackageListItem\n        v-if=\"pkg || proposal\"\n        :pkg=\"pkg || proposal\"\n        :to=\"{\n          name: pkg ? 'package' : 'package-proposal',\n          params: {\n            projectTypeSlug: (pkg || proposal).projectTypes[0].slug,\n            packageId: (pkg || proposal).id,\n          },\n        }\"\n        class=\"mt-4\"\n      />\n\n      <div\n        v-if=\"npmSearchResult && npmSearchResult.hits.length\"\n        class=\"flex flex-col mt-4\"\n      >\n        <div class=\"text-gray-500\">\n          Suggestions from NPM:\n        </div>\n\n        <BaseButton\n          v-for=\"result of npmSearchResult.hits\"\n          :key=\"result.id\"\n          class=\"bg-gray-800 hover:bg-gray-700 px-8 py-4 mt-4\"\n          @click=\"selectNpmSuggestion(result)\"\n        >\n          <div class=\"w-full text-left flex\">\n            <div class=\"flex-none mr-4\">\n              {{ result.name }}\n            </div>\n            <div\n              v-tooltip=\"result.description\"\n              class=\"text-gray-600 truncate\"\n            >\n              {{ result.description }}\n            </div>\n          </div>\n        </BaseButton>\n      </div>\n\n      <MultiSelect\n        id=\"tags\"\n        v-model=\"formData.tags\"\n        :options=\"availableTags\"\n        :close-on-select=\"false\"\n        multiple\n        taggable\n        placeholder=\"Enter tags\"\n        class=\"mt-8\"\n        @tag=\"tag => formData.tags.push(tag)\"\n      />\n\n      <div class=\"mt-8 flex items-center justify-end\">\n        <BaseButton\n          :loading=\"submitting\"\n          icon-left=\"add\"\n          class=\"bg-purple-800 hover:bg-purple-700 px-8 py-4\"\n          @click=\"submit()\"\n        >\n          Add package\n        </BaseButton>\n      </div>\n\n      <ErrorMessage\n        :error=\"error\"\n        class=\"error-box mt-8\"\n      />\n    </div>\n  </div>\n  <div v-else-if=\"addedProposal\">\n    <PackageAdded\n      :proposal=\"addedProposal\"\n      @add-another=\"addAnother()\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageAdded.vue",
    "content": "<script>\nimport PageTitle from '../PageTitle.vue'\n\nexport default {\n  components: {\n    PageTitle,\n  },\n\n  props: {\n    proposal: {\n      type: Object,\n      required: true,\n    },\n  },\n}\n</script>\n\n<template>\n  <div class=\"max-w-lg\">\n    <PageTitle>\n      {{ proposal.projectTypes[0].name }} package submitted!\n    </PageTitle>\n\n    <div class=\"flex items-center my-8\">\n      <i class=\"material-icons text-4xl text-purple-500 mr-4\">check_circle_outline</i>\n      <div>\n        Thanks for submitting this package!<br>\n        It will appear in the list when it's approved.<br>\n        Meanwhile, people can upvote your proposal, so <router-link\n          :to=\"{\n            name: 'package-proposal',\n            params: {\n              projectTypeSlug: proposal.projectTypes[0].slug,\n              packageId: proposal.id,\n            }\n          }\"\n          target=\"_blank\"\n          class=\"text-purple-500 hover:text-purple-400\"\n        >\n          share it\n        </router-link>!\n      </div>\n    </div>\n\n    <div class=\"text-gray-500\">\n      <div>\n        Name:\n        <span class=\"text-white\">{{ proposal.name }}</span>\n      </div>\n      <div>\n        Tags:\n        <span class=\"text-white\">{{ proposal.info.tags.join(', ') }}</span>\n      </div>\n    </div>\n\n    <div class=\"mt-8 flex items-center justify-end\">\n      <BaseButton\n        icon-left=\"add\"\n        class=\"px-8 py-4 bg-gray-800 hover:bg-gray-700 mr-4\"\n        @click=\"$emit('add-another')\"\n      >\n        Add another\n      </BaseButton>\n\n      <BaseButton\n        :to=\"{\n          name: 'package-proposal',\n          params: {\n            projectTypeSlug: proposal.projectTypes[0].slug,\n            packageId: proposal.id,\n          }\n        }\"\n        class=\"px-8 py-4 bg-purple-800 hover:bg-purple-700\"\n      >\n        View proposal\n      </BaseButton>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageBookmarkButton.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { useQuery, useResult, useMutation } from '@vue/apollo-composable'\n\nexport default {\n  props: {\n    packageId: {\n      type: String,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    // Package\n    const { result, loading } = useQuery(gql`\n      query PackageBookmarked ($id: ID!) {\n        pkg: package(id: $id) {\n          id\n          bookmarked\n        }\n      }\n    `, () => ({\n      id: props.packageId,\n    }))\n    const pkg = useResult(result, {})\n\n    // Toggle\n    const { mutate: toggle } = useMutation(gql`\n      mutation TogglePackageBookmark ($input: TogglePackageBookmarkInput!) {\n        togglePackageBookmark (input: $input) {\n          id\n          bookmarked\n        }\n      }\n    `, () => ({\n      variables: {\n        input: {\n          packageId: props.packageId,\n        },\n      },\n      optimisticResponse: {\n        __typename: 'Mutation',\n        togglePackageBookmark: {\n          __typename: 'Package',\n          id: props.packageId,\n          bookmarked: !pkg.value.bookmarked,\n        },\n      },\n    }))\n\n    return {\n      pkg,\n      loading,\n      toggle,\n    }\n  },\n}\n</script>\n\n<template>\n  <BaseButton\n    :disabled=\"loading\"\n    class=\"bg-gray-800 hover:bg-gray-700 px-8 py-4\"\n    :icon-left=\"pkg.bookmarked ? 'bookmark' : 'bookmark_border'\"\n    @click=\"toggle()\"\n  >\n    {{ pkg.bookmarked ? 'Bookmarked' : 'Bookmark' }}\n  </BaseButton>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageCount.vue",
    "content": "<script>\nimport millify from 'millify'\nimport { computed } from '@vue/composition-api'\n\nexport default {\n  props: {\n    count: {\n      type: [Number, String],\n      required: true,\n    },\n\n    icon: {\n      type: String,\n      default: 'star',\n    },\n\n    color: {\n      type: String,\n      default: 'purple',\n    },\n  },\n\n  setup (props) {\n    return {\n      formatted: computed(() => millify(parseInt(props.count) || 0, {\n        precision: 1,\n      })),\n    }\n  },\n}\n</script>\n\n<template>\n  <div\n    class=\"flex\"\n    :class=\"`text-${color}-500`\"\n  >\n    <div class=\"mr-1 sm:mr-2\">\n      {{ formatted }}\n    </div>\n    <i class=\"material-icons\">{{ icon }}</i>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageDataSource.vue",
    "content": "<script>\nexport default {\n  props: {\n    dataSource: {\n      type: Object,\n      required: true,\n    },\n  },\n}\n</script>\n\n<template>\n  <div class=\"p-4 mb-4 border-2 border-gray-800 rounded\">\n    <div class=\"text-xl mb-2\">\n      {{ dataSource.type }}\n    </div>\n    <div>\n      <div\n        v-for=\"(value, key) in dataSource.data\"\n        :key=\"key\"\n      >\n        <span class=\"text-gray-500 mr-2\">{{ key }}:</span>\n        <span>{{ value }}</span>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageDownloadsCount.vue",
    "content": "<script>\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\n\nimport PackageCount from './PackageCount.vue'\n\nexport default {\n  components: {\n    PackageCount,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { result, loading } = useQuery(gql`\n      query PackageLastMonthDownloads ($id: ID!) {\n        pkg: ${props.pkg.__typename === 'Package' ? 'package' : 'packageProposal'} (id: $id) {\n          id\n          insight {\n            npm {\n              downloads (range: month)\n            }\n          }\n        }\n      }\n    `, () => ({\n      id: props.pkg.id,\n    }))\n    const downloads = useResult(result, null, data => data.pkg.insight.npm.downloads)\n\n    return {\n      downloads,\n      loading,\n    }\n  },\n}\n</script>\n\n<template>\n  <PackageCount\n    v-if=\"!loading && downloads != null\"\n    v-tooltip=\"'NPM Downloads last month'\"\n    :count=\"downloads\"\n    icon=\"cloud_download\"\n  />\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageEditForm.vue",
    "content": "<script>\nimport { ref, watch } from '@vue/composition-api'\nimport omit from 'lodash/omit'\nimport { useAvailableTags } from '@/util/tags'\n\nimport ErrorMessage from '../ErrorMessage.vue'\nimport MultiSelect from 'vue-multiselect'\n\nexport default {\n  components: {\n    ErrorMessage,\n    MultiSelect,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n\n    submitting: {\n      type: Boolean,\n      default: false,\n    },\n\n    error: {\n      type: [String, Object, Error],\n      default: null,\n    },\n  },\n\n  setup (props, { emit }) {\n    const formData = ref()\n\n    function findDataSource (type) {\n      const dataSource = props.pkg.dataSources.find(d => d.type === type)\n      return dataSource ? dataSource.data : {}\n    }\n\n    function reset () {\n      formData.value = {\n        info: {\n          ...omit(props.pkg.info, ['__typename']),\n        },\n        dataSources: {\n          github: {\n            owner: '',\n            repo: '',\n            ...omit(findDataSource('github'), ['__typename']),\n          },\n          npm: {\n            name: '',\n            ...omit(findDataSource('npm'), ['__typename']),\n          },\n        },\n      }\n    }\n    reset()\n    // Auto reset after submit\n    watch(() => props.submitting, value => {\n      if (!value && !props.error) {\n        reset()\n      }\n    }, {\n      lazy: true,\n    })\n\n    function submit () {\n      const result = {\n        info: formData.value.info,\n        dataSources: {},\n      }\n\n      // Github\n      if (formData.value.dataSources.github.owner && formData.value.dataSources.github.repo) {\n        result.dataSources.github = formData.value.dataSources.github\n      }\n\n      // NPM\n      if (formData.value.dataSources.npm.name) {\n        result.dataSources.npm = formData.value.dataSources.npm\n      }\n\n      emit('submit', result)\n    }\n\n    // Auto split Github\n    const repoInput = ref()\n    watch(() => formData.value.dataSources.github.owner, value => {\n      if (value.includes('/')) {\n        const [owner, repo] = value.split('/')\n        Object.assign(formData.value.dataSources.github, {\n          owner,\n          repo,\n        })\n        repoInput.value.focus()\n      }\n    })\n\n    // Available tags\n    const { availableTags } = useAvailableTags(\n      () => props.pkg.projectTypes[0].id,\n      () => formData.value.info ? formData.value.info.tags : [],\n    )\n\n    return {\n      formData,\n      submit,\n      repoInput,\n      availableTags,\n    }\n  },\n}\n</script>\n\n<template>\n  <form\n    class=\"max-w-3xl\"\n    @submit.prevent=\"submit()\"\n  >\n    <div class=\"lg:flex items-baseline mt-4\">\n      <label\n        for=\"github-owner\"\n        class=\"flex-none lg:mr-8 lg:w-40 text-gray-500\"\n      >\n        GitHub DataSource:\n      </label>\n\n      <input\n        id=\"github-owner\"\n        v-model=\"formData.dataSources.github.owner\"\n        placeholder=\"GitHub owner name\"\n        maxlength=\"200\"\n        class=\"mt-2 lg:mt-0 bg-black px-8 py-4 rounded w-full\"\n      >\n\n      <input\n        ref=\"repoInput\"\n        v-model=\"formData.dataSources.github.repo\"\n        :required=\"!!formData.dataSources.github.owner\"\n        placeholder=\"GitHub repository name\"\n        maxlength=\"200\"\n        class=\"mt-4 lg:mt-0 lg:ml-8 bg-black px-8 py-4 rounded w-full\"\n      >\n    </div>\n\n    <div class=\"lg:flex items-baseline mt-8\">\n      <label\n        for=\"npm-name\"\n        class=\"flex-none lg:mr-8 lg:w-40 text-gray-500\"\n      >\n        NPM DataSource:\n      </label>\n\n      <input\n        id=\"npm-name\"\n        v-model=\"formData.dataSources.npm.name\"\n        placeholder=\"Package name on registry\"\n        maxlength=\"200\"\n        class=\"mt-2 lg:mt-0 bg-black px-8 py-4 rounded w-full\"\n      >\n    </div>\n\n    <div class=\"lg:flex items-center mt-8\">\n      <label\n        for=\"tags\"\n        class=\"flex-none lg:mr-8 lg:w-40 text-gray-500\"\n      >\n        Tags:\n      </label>\n\n      <MultiSelect\n        id=\"tags\"\n        v-model=\"formData.info.tags\"\n        :options=\"availableTags\"\n        :close-on-select=\"false\"\n        multiple\n        taggable\n        placeholder=\"Enter tags\"\n        @tag=\"tag => formData.info.tags.push(tag)\"\n      />\n    </div>\n\n    <div class=\"mt-8 flex items-center justify-end\">\n      <slot name=\"actions\" />\n\n      <BaseButton\n        :loading=\"submitting\"\n        type=\"submit\"\n        icon-left=\"save\"\n        class=\"bg-purple-800 hover:bg-purple-700 px-8 py-4\"\n      >\n        Save package\n      </BaseButton>\n    </div>\n\n    <ErrorMessage\n      :error=\"error\"\n      class=\"error-box mt-8\"\n    />\n  </form>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageEditProjectTypesForm.vue",
    "content": "<script>\nimport { ref, watch } from '@vue/composition-api'\n\nimport ErrorMessage from '../ErrorMessage.vue'\nimport ProjectTypeMultiSelect from '../project-type/ProjectTypeMultiSelect.vue'\n\nexport default {\n  components: {\n    ErrorMessage,\n    ProjectTypeMultiSelect,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n\n    submitting: {\n      type: Boolean,\n      default: false,\n    },\n\n    error: {\n      type: [String, Object, Error],\n      default: null,\n    },\n  },\n\n  setup (props, { emit }) {\n    const formData = ref()\n\n    function reset () {\n      formData.value = {\n        projectTypeIds: props.pkg.projectTypes.map(pt => pt.id),\n      }\n    }\n\n    reset()\n    // Auto reset after submit\n    watch(() => props.submitting, value => {\n      if (!value && !props.error) {\n        reset()\n      }\n    }, {\n      lazy: true,\n    })\n\n    function submit () {\n      const result = {\n        ...formData.value,\n      }\n\n      emit('submit', result)\n    }\n\n    return {\n      formData,\n      submit,\n    }\n  },\n}\n</script>\n\n<template>\n  <form\n    class=\"max-w-3xl\"\n    @submit.prevent=\"submit()\"\n  >\n    <div class=\"lg:flex items-center mt-4\">\n      <label\n        for=\"github-owner\"\n        class=\"flex-none lg:mr-8 lg:w-40 text-gray-500\"\n      >\n        Project types:\n      </label>\n      <ProjectTypeMultiSelect\n        v-model=\"formData.projectTypeIds\"\n      />\n    </div>\n\n    <div class=\"mt-8 flex items-center justify-end\">\n      <slot name=\"actions\" />\n\n      <BaseButton\n        :loading=\"submitting\"\n        type=\"submit\"\n        icon-left=\"save\"\n        class=\"bg-purple-800 hover:bg-purple-700 px-8 py-4\"\n      >\n        Save project types\n      </BaseButton>\n    </div>\n\n    <ErrorMessage\n      :error=\"error\"\n      class=\"error-box mt-8\"\n    />\n  </form>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageGeneralInfo.vue",
    "content": "<script>\nimport { useEmoji } from '@/util/emoji'\nimport PackageLogo from './PackageLogo.vue'\nimport PackageCount from './PackageCount.vue'\n\nexport default {\n  components: {\n    PackageLogo,\n    PackageCount,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { parsedText: parsedDescription } = useEmoji(() => props.pkg.description)\n\n    return {\n      parsedDescription,\n    }\n  },\n}\n</script>\n\n<template>\n  <div\n    class=\"flex items-center pb-4 lg:py-4\"\n  >\n    <PackageLogo\n      v-if=\"!$responsive.sm\"\n      :pkg=\"pkg\"\n      class=\"mr-6\"\n    />\n\n    <div class=\"flex-1 w-0 mr-6 overflow-hidden\">\n      <div class=\"w-full truncate text-gray-600\">\n        <span class=\"text-gray-100\">\n          {{ pkg.name }}\n        </span>\n\n        <span>\n          by {{ pkg.maintainers.map(m => m.name).join(', ') }}\n        </span>\n      </div>\n\n      <div class=\"w-full truncate text-gray-500\">\n        <span v-if=\"pkg.description\">\n          {{ parsedDescription }}\n        </span>\n        <span v-else>No description</span>\n      </div>\n    </div>\n\n    <slot />\n\n    <a\n      v-if=\"pkg.repo\"\n      :href=\"pkg.repo\"\n      target=\"_blank\"\n    >\n      <PackageCount\n        v-tooltip=\"'GitHub stars'\"\n        :count=\"pkg.stars || 0\"\n      />\n    </a>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageInsightNpmDownloads.vue",
    "content": "<script>\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\nimport { computed } from '@vue/composition-api'\nimport { DateTime } from 'luxon'\nimport millify from 'millify'\n\nimport DotChart from '../chart/DotChart.vue'\n\nexport default {\n  components: {\n    DotChart,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { result, loading } = useQuery(gql`\n      query PackageInsightNpmDownloads ($id: ID!) {\n        pkg:${props.pkg.__typename === 'Package' ? 'package' : 'packageProposal'} (id: $id) {\n          id\n          insight {\n            npm {\n              downloadsPoints (range: month) {\n                downloads\n                day\n              }\n            }\n          }\n        }\n      }\n    `, () => ({\n      id: props.pkg.id,\n    }))\n    const downloads = useResult(result, [], data => data.pkg.insight.npm.downloadsPoints)\n    const points = computed(() => downloads.value.map(point => ({\n      value: point.downloads,\n      label: DateTime.fromMillis(point.day).toFormat('MMM d'),\n      tooltip: `<b>${millify(point.downloads, {\n        precision: 1,\n      })}</b> <span class=\"text-gray-700\">downloads on ${DateTime.fromMillis(point.day).toLocaleString()}</span>`,\n    })))\n\n    return {\n      points,\n      loading,\n    }\n  },\n}\n</script>\n\n<template>\n  <DotChart\n    :loading=\"loading\"\n    :points=\"points\"\n    title=\"NPM Downloads\"\n    icon=\"cloud_download\"\n    class=\"max-w-192\"\n  />\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageInstallButton.vue",
    "content": "<script>\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\nimport { computed } from '@vue/composition-api'\nexport default {\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    // Package\n    const { result } = useQuery(() => gql`\n      query PackageBookmarked ($id: ID!) {\n        pkg: ${props.pkg.__typename === 'Package' ? 'package' : 'packageProposal'} (id: $id) {\n          id\n          dataSources {\n            type\n            data\n          }\n        }\n      }\n    `, () => ({\n      id: props.pkg.id,\n    }))\n    const dataSources = useResult(result, [], data => data.pkg.dataSources)\n\n    const npmDataSource = computed(() => dataSources.value.find(ds => ds.type === 'npm'))\n    const npmName = computed(() => npmDataSource.value ? npmDataSource.value.data.name : null)\n\n    // Installation\n\n    function install () {\n      if (parent) {\n        parent.postMessage({\n          awesomeInstall: npmName.value,\n        }, '*')\n      }\n    }\n\n    return {\n      npmName,\n      install,\n    }\n  },\n}\n</script>\n\n<template>\n  <BaseButton\n    :disabled=\"!npmName\"\n    class=\"bg-purple-800 hover:bg-purple-700 px-8 py-4\"\n    icon-left=\"get_app\"\n    @click=\"install()\"\n  >\n    Install\n  </BaseButton>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageInstallationManager.vue",
    "content": "<script>\nimport { installationAvailable } from '@/util/installation'\n\nexport default {\n  setup () {\n    window.addEventListener('message', event => {\n      if (event.data && event.data.awesomeInstallationAvailable) {\n        installationAvailable.value = true\n      }\n    })\n  },\n\n  render () {\n    return null\n  },\n}\n</script>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageLinks.vue",
    "content": "<script>\nimport { computed } from '@vue/composition-api'\nexport default {\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const links = computed(() => {\n      const list = []\n      if (props.pkg.homepage) {\n        list.push({\n          url: props.pkg.homepage,\n          label: 'Homepage',\n        })\n      }\n      if (props.pkg.repo) {\n        list.push({\n          url: props.pkg.repo,\n          label: 'Repository',\n        })\n      } else if (props.pkg.__typename === 'PackageProposal') {\n        list.push({\n          url: `https://www.npmjs.com/search?q=${encodeURIComponent(props.pkg.name)}`,\n          label: 'Search on NPM',\n        })\n      }\n      return list\n    })\n\n    return {\n      links,\n    }\n  },\n}\n</script>\n\n<template>\n  <div v-if=\"links.length\">\n    <a\n      v-for=\"(link, index) of links\"\n      :key=\"index\"\n      :href=\"link.url\"\n      target=\"_blank\"\n      class=\"inline-block py-2 mr-8 text-gray-500 hover:text-gray-400\"\n    >\n      {{ link.label }}\n      <i class=\"material-icons text-lg\">open_in_new</i>\n    </a>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageList.vue",
    "content": "<script>\nimport EmptyMessage from '../EmptyMessage.vue'\nimport PackageAddButton from './PackageAddButton.vue'\nimport PackageListItem from './PackageListItem.vue'\nimport LoadingIndicator from '../LoadingIndicator.vue'\n\nimport gql from 'graphql-tag'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { ref, computed } from '@vue/composition-api'\nimport mergeWith from 'lodash/mergeWith'\nimport { pkgFragment } from './fragments'\nimport { onScrollBottom } from '@/util/scroll'\n\nexport default {\n  components: {\n    EmptyMessage,\n    PackageAddButton,\n    PackageListItem,\n    LoadingIndicator,\n  },\n\n  props: {\n    projectTypeSlug: {\n      type: String,\n      required: true,\n    },\n\n    tags: {\n      type: Array,\n      default: null,\n    },\n  },\n\n  setup (props) {\n    const { result, loading, fetchMore } = useQuery(gql`\n      query ProjectTypePackages ($slug: String!, $tags: [String!], $after: JSON) {\n        projectType: projectTypeBySlug (slug: $slug) {\n          id\n          packages (tags: $tags, after: $after)\n              @connection(key: \"ProjectTypePackages_packages\", filter: [\"tags\"]) {\n            items {\n              ...pkg\n            }\n            after\n          }\n        }\n      }\n      ${pkgFragment}\n    `, () => ({\n      slug: props.projectTypeSlug,\n      tags: props.tags,\n    }))\n    const packages = useResult(result, [], data => data.projectType.packages.items)\n\n    // Pagination\n    const afterCursor = computed(() => result.value ? result.value.projectType.packages.after : null)\n    const hasMore = computed(() => !!afterCursor.value)\n    const loadingMore = ref(false)\n    async function loadMore () {\n      if (loadingMore.value) return\n      if (afterCursor.value) {\n        loadingMore.value = true\n        await fetchMore({\n          variables: {\n            after: afterCursor.value,\n          },\n          updateQuery: (previousResult, { fetchMoreResult }) => {\n            return mergeWith({}, previousResult, fetchMoreResult, (target, source, key) => {\n              if (key !== 'after' && Array.isArray(target)) {\n                return target.concat(source)\n              }\n            })\n          },\n        })\n        loadingMore.value = false\n      }\n    }\n\n    // Autoload when scrolling\n    const el = ref()\n    onScrollBottom(loadMore, el, 600)\n\n    return {\n      packages,\n      loading,\n      hasMore,\n      loadMore,\n      loadingMore,\n      el,\n    }\n  },\n}\n</script>\n\n<template>\n  <div ref=\"el\">\n    <LoadingIndicator\n      v-if=\"loading && !packages.length\"\n      class=\"py-8\"\n    />\n    <template v-else-if=\"packages.length\">\n      <PackageListItem\n        v-for=\"pkg of packages\"\n        :key=\"pkg.id\"\n        :to=\"{\n          name: $route.path.includes('/pkg/') && $route.params.packageId !== pkg.id ? undefined : 'package',\n          params: { packageId: pkg.id },\n        }\"\n        :pkg=\"pkg\"\n        class=\"mb-4 sm:mb-6\"\n      />\n      <BaseButton\n        v-if=\"hasMore\"\n        :loading=\"loadingMore\"\n        class=\"w-full bg-gray-800 text-purple-300 rounded px-6 py-4 hover:bg-gray-700\"\n        @click=\"loadMore()\"\n      >\n        Load more\n      </BaseButton>\n    </template>\n    <EmptyMessage v-else>\n      No package yet\n\n      <template #cta>\n        <PackageAddButton\n          class=\"bg-gray-800 hover:bg-gray-700\"\n        />\n      </template>\n    </EmptyMessage>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageListItem.vue",
    "content": "<script>\nimport PackageLogo from './PackageLogo.vue'\nimport PackageCount from './PackageCount.vue'\nimport { useEmoji } from '@/util/emoji'\nimport { useTags } from '@/util/tags'\n\nexport default {\n  components: {\n    PackageLogo,\n    PackageCount,\n  },\n\n  inheritAttrs: false,\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { isOfficial } = useTags(() => props.pkg)\n    const { parsedText: parsedDescription } = useEmoji(() => props.pkg.description)\n\n    return {\n      isOfficial,\n      parsedDescription,\n    }\n  },\n}\n</script>\n\n<template>\n  <router-link\n    v-bind=\"$attrs\"\n    class=\"package-list-item button-like flex items-center bg-gray-800 rounded p-2 sm:px-6 sm:py-4 hover:bg-gray-700 leading-snug sm:leading-normal\"\n  >\n    <PackageLogo\n      :pkg=\"pkg\"\n      class=\"mr-2 sm:mr-6\"\n    />\n\n    <div class=\"flex-1 w-0 mr-2 sm:mr-6 overflow-hidden\">\n      <div class=\"w-full truncate text-gray-600\">\n        <span class=\"text-gray-100\">\n          {{ pkg.name }}\n        </span>\n\n        <!-- <span\n          v-if=\"isOfficial\"\n          class=\"text-orange-400 bg-yellow-900 px-1 mx-1 rounded\"\n        >\n          official\n        </span> -->\n      </div>\n\n      <div class=\"w-full truncate text-gray-500\">\n        <span v-if=\"pkg.description\">\n          {{ parsedDescription }}\n        </span>\n        <span v-else>No description</span>\n      </div>\n    </div>\n\n    <slot />\n\n    <PackageCount\n      v-if=\"pkg.upvotes != null\"\n      :count=\"pkg.upvotes\"\n      icon=\"thumb_up\"\n      class=\"package-count\"\n    />\n    <PackageCount\n      v-else-if=\"pkg.stars != null\"\n      :count=\"pkg.stars\"\n      :color=\"isOfficial ? 'orange' : 'purple'\"\n      class=\"package-count\"\n    />\n  </router-link>\n</template>\n\n<style lang=\"postcss\" scoped>\n.package-list-item.router-link-active {\n  @apply bg-purple-900;\n\n  &:hover {\n    @apply bg-purple-800;\n  }\n\n  .text-gray-100 {\n    @apply text-purple-100;\n  }\n\n  .text-gray-600 {\n    @apply text-purple-600;\n  }\n\n  .text-gray-500 {\n    @apply text-purple-500;\n  }\n\n  .package-count {\n    @apply text-purple-400;\n\n    &.text-orange-500 {\n      @apply text-orange-400;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageLogo.vue",
    "content": "<script>\nimport { ref, watch } from '@vue/composition-api'\nimport genericLogo from '@/assets/package.png'\n\nexport default {\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const src = ref(null)\n\n    watch(() => props.pkg.id, async id => {\n      src.value = null\n      await tryLogo(id, 'logo.svg')\n      await tryLogo(id, 'logo.png')\n    })\n\n    function tryLogo (id, filename) {\n      return new Promise(resolve => {\n        const checkId = () => id === props.pkg.id\n\n        if (!checkId()) {\n          return resolve(false)\n        }\n\n        const img = new Image()\n        img.onload = () => {\n          if (!checkId()) {\n            resolve(false)\n          } else {\n            src.value = img.src\n          }\n        }\n        img.onerror = () => {\n          resolve(false)\n        }\n        img.src = `https://unpkg.com/${props.pkg.name}/${filename}`\n      })\n    }\n\n    function onError () {\n      src.value = genericLogo\n    }\n\n    return {\n      src,\n      genericLogo,\n      onError,\n    }\n  },\n}\n</script>\n\n<template>\n  <div class=\"w-10 h-10 flex items-center justify-center\">\n    <img\n      :src=\"src || pkg.defaultLogo || genericLogo\"\n      :alt=\"`${pkg.name} logo`\"\n      class=\"max-w-full max-h-full rounded overflow-hidden\"\n      @error=\"onError()\"\n    >\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageProposalApproveButton.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { pkgFragment } from './fragments'\nimport { useMutation } from '@vue/apollo-composable'\nimport { useSelectNextProposal, removeProposalFromCache } from '@/util/proposal'\n\nexport default {\n  props: {\n    projectTypeId: {\n      type: String,\n      required: true,\n    },\n\n    proposal: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    // Action\n    const { mutate } = useMutation(gql`\n      mutation ApprovePackageProposal ($input: ApprovePackageProposalInput!) {\n        approvePackageProposal (input: $input) {\n          ...pkg\n        }\n      }\n      ${pkgFragment}\n    `, () => {\n      // Save current proposal ID as it will change since we immediately select another proposal\n      const currentProposalId = props.proposal.id\n      return {\n        variables: {\n          input: {\n            proposalId: currentProposalId,\n          },\n        },\n        update: (cache, { data: { approvePackageProposal } }) => {\n          removeProposalFromCache(cache, props.projectTypeId, currentProposalId)\n        },\n        optimisticResponse: {\n          __typename: 'Mutation',\n          approvePackageProposal: {\n            __typename: 'Package',\n            ...props.proposal,\n          },\n        },\n      }\n    })\n\n    const { selectNext } = useSelectNextProposal()\n\n    async function approve () {\n      await selectNext(props.projectTypeId, props.proposal.id)\n\n      // Approve mutation\n      await mutate()\n    }\n\n    return {\n      approve,\n    }\n  },\n}\n</script>\n\n<template>\n  <BaseButton\n    :disabled=\"!proposal.repo\"\n    icon-left=\"done\"\n    class=\"bg-orange-700 hover:bg-orange-800\"\n    @click=\"approve()\"\n  >\n    Approve\n  </BaseButton>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageProposalList.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { useQuery, useResult, useQueryLoading } from '@vue/apollo-composable'\nimport { useCurrentUser } from '../user/useCurrentUser'\nimport { pkgProposalFragment } from './fragments'\n\nimport EmptyMessage from '../EmptyMessage.vue'\nimport PackageAddButton from './PackageAddButton.vue'\nimport PackageListItem from './PackageListItem.vue'\nimport LoadingIndicator from '../LoadingIndicator.vue'\n\nexport default {\n  components: {\n    EmptyMessage,\n    PackageAddButton,\n    PackageListItem,\n    LoadingIndicator,\n  },\n\n  props: {\n    projectTypeId: {\n      type: String,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { result } = useQuery(gql`\n      query ProjectTypePackages ($id: ID!) {\n        projectType (id: $id) {\n          id\n          packageProposals {\n            ...pkgProposal\n          }\n        }\n      }\n      ${pkgProposalFragment}\n    `, () => ({\n      id: props.projectTypeId,\n    }), {\n      fetchPolicy: 'cache-and-network',\n    })\n    const proposals = useResult(result, [], data => data.projectType.packageProposals)\n\n    return {\n      loading: useQueryLoading(),\n      proposals,\n      currentUser: useCurrentUser().currentUser,\n    }\n  },\n}\n</script>\n\n<template>\n  <div>\n    <LoadingIndicator\n      v-if=\"loading && !proposals.length\"\n      class=\"py-8\"\n    />\n    <template v-else-if=\"proposals.length\">\n      <PackageListItem\n        v-for=\"pkg of proposals\"\n        :key=\"pkg.id\"\n        :pkg=\"pkg\"\n        :to=\"{\n          name: $route.path.includes('/pkg/') && $route.params.packageId !== pkg.id ? undefined : 'package-proposal',\n          params: { packageId: pkg.id },\n        }\"\n        class=\"mb-6\"\n      >\n        <i\n          v-if=\"!pkg.repo\"\n          v-tooltip=\"'No repository found'\"\n          class=\"material-icons text-lg mr-2 text-red-500\"\n        >error</i>\n      </PackageListItem>\n    </template>\n    <EmptyMessage v-else>\n      No proposal yet\n\n      <template #cta>\n        <PackageAddButton\n          class=\"bg-gray-800 hover:bg-gray-700\"\n        />\n      </template>\n    </EmptyMessage>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageProposalRemoveButton.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { useMutation } from '@vue/apollo-composable'\nimport { useSelectNextProposal, removeProposalFromCache } from '@/util/proposal'\n\nexport default {\n  props: {\n    projectTypeId: {\n      type: String,\n      required: true,\n    },\n\n    proposal: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    // Action\n    const { mutate } = useMutation(gql`\n      mutation RemovePackageProposal ($input: RemovePackageProposalInput!) {\n        removePackageProposal (input: $input)\n      }\n    `, () => {\n      // Save current proposal ID as it will change since we immediately select another proposal\n      const currentProposalId = props.proposal.id\n      return {\n        variables: {\n          input: {\n            id: currentProposalId,\n          },\n        },\n        update: (cache) => {\n          removeProposalFromCache(cache, props.projectTypeId, currentProposalId)\n        },\n        optimisticResponse: {\n          __typename: 'Mutation',\n          removePackageProposal: true,\n        },\n      }\n    })\n\n    const { selectNext } = useSelectNextProposal()\n\n    async function remove () {\n      await selectNext(props.projectTypeId, props.proposal.id)\n\n      // Approve mutation\n      await mutate()\n    }\n\n    return {\n      remove,\n    }\n  },\n}\n</script>\n\n<template>\n  <BaseButton\n    icon-left=\"delete_forever\"\n    class=\"bg-red-700 hover:bg-red-800\"\n    @click=\"remove()\"\n  >\n    Delete\n  </BaseButton>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageProposalTabEdit.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { useMutation, useQuery, useResult } from '@vue/apollo-composable'\nimport { pkgProposalFragment } from './fragments'\n\nimport LoadingIndicator from '../LoadingIndicator.vue'\nimport PackageEditForm from './PackageEditForm.vue'\nimport PackageEditProjectTypesForm from './PackageEditProjectTypesForm.vue'\n\nexport default {\n  components: {\n    LoadingIndicator,\n    PackageEditForm,\n    PackageEditProjectTypesForm,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { result, loading } = useQuery(gql`\n      query PackageEditInfo ($id: ID!) {\n        packageProposal (id: $id) {\n          id\n          info {\n            tags\n          }\n          dataSources {\n            type\n            data\n          }\n          projectTypes {\n            id\n          }\n        }\n      }\n    `, () => ({\n      id: props.pkg.id,\n    }))\n    const pkgEditInfo = useResult(result, { dataSources: [] })\n\n    // Package info\n\n    const { mutate, loading: submitting, error } = useMutation(gql`\n      mutation EditPackageProposalInfo ($input: EditPackageProposalInfoInput!) {\n        editPackageProposalInfo (input: $input) {\n          ...pkgProposal\n          dataSources {\n            type\n            data\n          }\n        }\n      }\n      ${pkgProposalFragment}\n    `)\n\n    async function editProposal (data) {\n      await mutate({\n        input: {\n          common: {\n            id: props.pkg.id,\n            ...data,\n          },\n        },\n      })\n    }\n\n    // Project types\n\n    const {\n      mutate: mutateProjectTypes,\n      loading: submittingProjectTypes,\n      error: projectTypesError,\n    } = useMutation(gql`\n      mutation EditPackageProposalInfo ($input: EditPackageProjectTypesInput!) {\n        editPackageProposalProjectTypes (input: $input) {\n          id\n          projectTypes {\n            id\n          }\n        }\n      }\n    `)\n\n    async function editProjectTypes (data) {\n      await mutateProjectTypes({\n        input: {\n          packageId: props.pkg.id,\n          ...data,\n        },\n      })\n    }\n\n    return {\n      loading,\n      pkgEditInfo,\n\n      editProposal,\n      submitting,\n      error,\n\n      editProjectTypes,\n      submittingProjectTypes,\n      projectTypesError,\n    }\n  },\n}\n</script>\n\n<template>\n  <LoadingIndicator\n    v-if=\"loading\"\n    class=\"mt-8\"\n  />\n\n  <div\n    v-else\n    class=\"pb-64\"\n  >\n    <PackageEditForm\n      :pkg=\"pkgEditInfo\"\n      :submitting=\"submitting\"\n      :error=\"error\"\n      @submit=\"editProposal\"\n    />\n\n    <hr class=\"border-gray-800 my-8 max-w-3xl\">\n\n    <PackageEditProjectTypesForm\n      :pkg=\"pkgEditInfo\"\n      :submitting=\"submittingProjectTypes\"\n      :error=\"projectTypesError\"\n      @submit=\"editProjectTypes\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageProposalTabGeneral.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\n\nimport PackageLinks from './PackageLinks.vue'\nimport PackageTags from './PackageTags.vue'\nconst PackageReadme = () => import(\n  /* webpackChunkName: \"PackageReadme.vue\" */\n  './PackageReadme.vue'\n)\n\nexport default {\n  components: {\n    PackageLinks,\n    PackageReadme,\n    PackageTags,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup () {\n    const readmeQuery = gql`\n      query PackageProposalReadme ($id: ID!) {\n        pkg: packageProposal (id: $id) {\n          id\n          readme\n        }\n      }\n    `\n\n    return {\n      readmeQuery,\n    }\n  },\n}\n</script>\n\n<template>\n  <div>\n    <PackageLinks\n      :pkg=\"pkg\"\n      class=\"mb-4\"\n    />\n\n    <PackageTags\n      :pkg=\"pkg\"\n    />\n\n    <PackageReadme\n      v-if=\"pkg.repo\"\n      :package-id=\"pkg.id\"\n      :query=\"readmeQuery\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageProposalUpvoteButton.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { useQuery, useResult, useMutation } from '@vue/apollo-composable'\n\nexport default {\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    // Is Upvoted\n    const { result, loading } = useQuery(gql`\n      query PackageProposalUpvoted ($id: ID!) {\n        packageProposal (id: $id) {\n          id\n          upvoted\n        }\n      }\n    `, () => ({\n      id: props.pkg.id,\n    }))\n    const upvoted = useResult(result, false, data => data.packageProposal.upvoted)\n\n    // Toggle\n    const { mutate: toggle } = useMutation(gql`\n      mutation TogglePackageProposalUpvote ($input: TogglePackageProposalUpvoteInput!) {\n        togglePackageProposalUpvote (input: $input) {\n          id\n          upvotes\n          upvoted\n        }\n      }\n    `, () => ({\n      variables: {\n        input: {\n          proposalId: props.pkg.id,\n        },\n      },\n      optimisticResponse: {\n        __typename: 'Mutation',\n        togglePackageProposalUpvote: {\n          __typename: 'PackageProposal',\n          id: props.pkg.id,\n          upvoted: !upvoted.value,\n          upvotes: upvoted.value ? props.pkg.upvotes - 1 : props.pkg.upvotes + 1,\n        },\n      },\n    }))\n\n    return {\n      upvoted,\n      loading,\n      toggle,\n    }\n  },\n}\n</script>\n\n<template>\n  <BaseButton\n    :disabled=\"loading\"\n    class=\"bg-gray-800 hover:bg-gray-700 px-8 py-4\"\n    :icon-left=\"upvoted ? 'check_box' : 'check_box_outline_blank'\"\n    @click=\"toggle()\"\n  >\n    <b class=\"font-bold mr-1\">{{ pkg.upvotes }}</b>\n    upvote{{ pkg.upvotes > 1 ? 's' : '' }}\n  </BaseButton>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageProposalView.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { pkgProposalFragment } from './fragments'\nimport { computed } from '@vue/composition-api'\nimport { installationAvailable } from '@/util/installation'\n\nimport PackageViewLayout from './PackageViewLayout.vue'\nimport PackageProposalUpvoteButton from './PackageProposalUpvoteButton.vue'\nimport PackageProposalApproveButton from './PackageProposalApproveButton.vue'\nimport PackageProposalRemoveButton from './PackageProposalRemoveButton.vue'\nimport PackageInstallButton from './PackageInstallButton.vue'\nimport PackageReleaseCount from './PackageReleaseCount.vue'\nimport RouteTab from '../RouteTab.vue'\n\nexport default {\n  components: {\n    PackageViewLayout,\n    PackageProposalApproveButton,\n    PackageProposalRemoveButton,\n    PackageProposalUpvoteButton,\n    PackageReleaseCount,\n    PackageInstallButton,\n    RouteTab,\n  },\n\n  props: {\n    packageId: {\n      type: String,\n      required: true,\n    },\n\n    projectTypeId: {\n      type: String,\n      required: true,\n    },\n  },\n\n  setup (props, { root }) {\n    const { result, loading } = useQuery(gql`\n      query PackageProposal ($id: ID!) {\n        pkg: packageProposal (id: $id) {\n          ...pkgProposal\n        }\n      }\n      ${pkgProposalFragment}\n    `, () => ({\n      id: props.packageId,\n    }))\n    const pkg = useResult(result)\n\n    const { result: additionalResult } = useQuery(gql`\n      query PackageProposalProjectTypes ($packageId: ID!) {\n        pkg: packageProposal (id: $packageId) {\n          id\n          projectTypes {\n            id\n            inTeam\n          }\n        }\n      }\n    `, props)\n    const inTeam = computed(() => {\n      if (additionalResult.value) {\n        return additionalResult.value.pkg.projectTypes.some(pt => pt.inTeam)\n      }\n    })\n\n    return {\n      pkg,\n      loading,\n      inTeam,\n      installationAvailable,\n    }\n  },\n\n  metaInfo () {\n    if (!this.pkg) return\n\n    return {\n      title: `[Proposal] ${this.pkg.name}`,\n    }\n  },\n}\n</script>\n\n<template>\n  <PackageViewLayout\n    :pkg=\"pkg\"\n    :loading=\"loading\"\n    :error=\"pkg && !pkg.repo ? 'No GitHub repository found' : null\"\n  >\n    <template #actions>\n      <template v-if=\"inTeam\">\n        <PackageInstallButton\n          v-if=\"installationAvailable\"\n          :pkg=\"pkg\"\n          class=\"mr-4\"\n        />\n\n        <PackageProposalApproveButton\n          :project-type-id=\"projectTypeId\"\n          :proposal=\"pkg\"\n          class=\"px-8 py-4 mr-4\"\n        />\n\n        <PackageProposalRemoveButton\n          :project-type-id=\"projectTypeId\"\n          :proposal=\"pkg\"\n          class=\"px-8 py-4 mr-4\"\n        />\n      </template>\n\n      <PackageProposalUpvoteButton\n        :pkg=\"pkg\"\n      />\n    </template>\n\n    <template #tabs>\n      <RouteTab\n        :to=\"{ name: 'package-proposal' }\"\n        class=\"flex-none\"\n        exact\n      >\n        General\n      </RouteTab>\n\n      <RouteTab\n        :to=\"{ name: 'package-proposal-releases' }\"\n        class=\"flex-none\"\n      >\n        Releases\n        <PackageReleaseCount\n          v-if=\"!loading\"\n          :pkg=\"pkg\"\n        />\n      </RouteTab>\n\n      <RouteTab\n        :to=\"{ name: 'package-proposal-insight' }\"\n        class=\"flex-none\"\n      >\n        Insight\n      </RouteTab>\n\n      <RouteTab\n        :to=\"{ name: 'package-proposal-data-sources' }\"\n        class=\"flex-none\"\n      >\n        Data sources\n      </RouteTab>\n\n      <RouteTab\n        v-if=\"inTeam\"\n        :to=\"{ name: 'package-proposal-edit' }\"\n        class=\"flex-none\"\n      >\n        Edit\n      </RouteTab>\n    </template>\n  </PackageViewLayout>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageReadme.vue",
    "content": "<script>\nimport LoadingIndicator from '../LoadingIndicator.vue'\n\nimport Vue from 'vue'\nimport gql from 'graphql-tag'\nimport { ref, watch } from '@vue/composition-api'\nimport { useQuery, useResult } from '@vue/apollo-composable'\n\nconst GOOGLE_IMG_PROXY = 'https://images1-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&refresh=2592000&url='\n\nexport default {\n  components: {\n    LoadingIndicator,\n  },\n\n  props: {\n    packageId: {\n      type: String,\n      required: true,\n    },\n\n    query: {\n      type: Object,\n      default: null,\n    },\n  },\n\n  setup (props, { root }) {\n    // Pkg\n    const { result, loading } = useQuery(() => props.query || gql`\n      query PackageReadme ($id: ID!) {\n        pkg: package (id: $id) {\n          id\n          readme\n        }\n      }\n    `, () => ({\n      id: props.packageId,\n    }))\n    const pkg = useResult(result, {})\n\n    // Processing\n    const render = ref()\n    async function processRender () {\n      await Vue.nextTick()\n\n      // Images\n      const imgs = render.value.querySelectorAll('img')\n      for (const el of imgs) {\n        const img = new Image()\n        img.crossOrigin = 'Anonymous'\n        img.onload = () => onImgLoad(img, el)\n        img.src = `${GOOGLE_IMG_PROXY}${el.src}`\n      }\n    }\n\n    function onImgLoad (img, el) {\n      // Empty\n      if (img.width <= 1 || img.height <= 1) return\n\n      // Badges\n      if (img.height === 20) return\n\n      // Calculate brightness\n      const canvas = document.createElement('canvas')\n      canvas.width = img.width\n      canvas.height = img.height\n\n      const ctx = canvas.getContext('2d')\n      ctx.drawImage(img, 0, 0)\n\n      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)\n      const data = imageData.data\n      let r, g, b, avg\n      let colorSum = 0\n\n      for (let x = 0, len = data.length; x < len; x += 4) {\n        r = data[x]\n        g = data[x + 1]\n        b = data[x + 2]\n\n        avg = Math.floor((r + g + b) / 3)\n        colorSum += avg\n      }\n\n      const brightness = Math.floor(colorSum / (img.width * img.height))\n      if (brightness < 60) {\n        el.classList.add('ally-bg')\n\n        if (img.width === img.height && img.width <= 64) {\n          el.classList.add('avatar')\n        }\n      }\n    }\n\n    watch(() => pkg.value.readme, () => processRender(), {\n      lazy: true,\n    })\n\n    return {\n      pkg,\n      loading,\n      render,\n    }\n  },\n}\n</script>\n\n<template>\n  <LoadingIndicator\n    v-if=\"loading\"\n    class=\"p-8\"\n  />\n  <div\n    v-else\n    class=\"readme pb-8 mt-4 border-t-2 border-gray-800\"\n  >\n    <div\n      ref=\"render\"\n      class=\"markdown pt-8 pb-64\"\n      v-html=\"pkg.readme\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageRelease.vue",
    "content": "<script>\nimport { DateTime } from 'luxon'\nimport { computed } from '@vue/composition-api'\n\nimport PackageReleaseAsset from './PackageReleaseAsset.vue'\n\nexport default {\n  components: {\n    PackageReleaseAsset,\n  },\n\n  props: {\n    release: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const dateTime = computed(() => DateTime.fromMillis(props.release.date))\n    const fromNow = computed(() => dateTime.value.toRelative())\n    const humanDate = computed(() => dateTime.value.toLocaleString(DateTime.DATETIME_FULL))\n\n    return {\n      fromNow,\n      humanDate,\n    }\n  },\n}\n</script>\n\n<template>\n  <div class=\"p-6 mb-8 border-2 border-gray-800 rounded\">\n    <div class=\"text-gray-400\">\n      <span v-tooltip=\"humanDate\">\n        <i class=\"material-icons text-lg text-gray-500\">access_time</i>\n        <span class=\"text-sm ml-1\">{{ fromNow }}</span>\n      </span>\n    </div>\n\n    <div class=\"flex items-baseline\">\n      <div class=\"text-3xl text-purple-300\">\n        {{ release.title }}\n      </div>\n      <div class=\"text-gray-600 ml-2\">\n        <i class=\"material-icons text-base\">local_offer</i>\n        {{ release.tagName }}\n      </div>\n\n      <div\n        v-if=\"release.prerelease\"\n        class=\"ml-2 text-orange-500 border border-yellow-900 rounded px-2\"\n      >\n        prerelease\n      </div>\n    </div>\n\n    <div\n      v-if=\"release.assets.length\"\n      class=\"my-8\"\n    >\n      <PackageReleaseAsset\n        v-for=\"(asset, index) of release.assets\"\n        :key=\"index\"\n        :asset=\"asset\"\n        class=\"mb-4\"\n      />\n    </div>\n\n    <div\n      v-if=\"release.description\"\n      class=\"markdown mt-4\"\n      v-html=\"release.description\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageReleaseAsset.vue",
    "content": "<script>\nimport { computed } from '@vue/composition-api'\nimport millify from 'millify'\n\nexport default {\n  props: {\n    asset: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const formattedSize = computed(() => `${millify(props.asset.size)}B`)\n\n    return {\n      formattedSize,\n    }\n  },\n}\n</script>\n\n<template>\n  <div class=\"flex items-center\">\n    <BaseButton\n      :href=\"asset.downloadUrl\"\n      target=\"_blank\"\n      class=\"hover:text-purple-500\"\n    >\n      <i class=\"material-icons mr-2\">cloud_download</i>\n      {{ asset.name }}\n    </BaseButton>\n    <span class=\"ml-2 text-gray-600\">\n      {{ formattedSize }}\n    </span>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageReleaseCount.vue",
    "content": "<script>\nimport { useQuery } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\nexport default {\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { result, loading } = useQuery(gql`\n      query releaseCount ($id: ID!) {\n        pkg: ${props.pkg.__typename === 'Package' ? 'package' : 'packageProposal'} (id: $id) {\n          id\n          releaseCount\n          tagCount\n        }\n      }\n    `, () => ({\n      id: props.pkg.id,\n    }))\n    return {\n      result,\n      loading,\n    }\n  },\n}\n</script>\n\n<template>\n  <div\n    v-if=\"!loading && result.pkg.releaseCount\"\n    v-tooltip=\"`<b>${result.pkg.releaseCount}</b> releases<br><b>${result.pkg.tagCount}</b> tags`\"\n    class=\"ml-2 text-gray-400 bg-gray-800 text-xs px-1 rounded -mr-2\"\n  >\n    {{ result.pkg.releaseCount }}\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageShareButton.vue",
    "content": "<script>\nimport { ref } from '@vue/composition-api'\nconst PackageShareModal = () => import(\n  /* webpackChunkName: \"PackageShareModal.vue\" */\n  './PackageShareModal.vue'\n)\n\nexport default {\n  components: {\n    PackageShareModal,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup () {\n    const open = ref(false)\n\n    return {\n      open,\n    }\n  },\n}\n</script>\n\n<template>\n  <div>\n    <BaseButton\n      icon-left=\"share\"\n      class=\"bg-gray-800 hover:bg-gray-700 px-8 py-4\"\n      @click=\"open = true\"\n    >\n      Share\n    </BaseButton>\n\n    <PackageShareModal\n      v-if=\"open\"\n      :pkg=\"pkg\"\n      @close=\"open = false\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageShareModal.vue",
    "content": "<script>\nimport PopupModal from '../PopupModal.vue'\nimport { ref } from '@vue/composition-api'\nimport { useQrcode } from '@/util/qrcode'\nimport { useShare } from '@/util/share'\nimport { useRouter } from '@/util/router'\n\nexport default {\n  components: {\n    PopupModal,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props, { emit }) {\n    function close () {\n      emit('close')\n    }\n\n    const router = useRouter()\n    const url = ref(window.location.origin + router.resolve({\n      name: 'package',\n      params: {\n        packageId: props.pkg.id,\n      },\n    }).href)\n\n    const input = ref(null)\n\n    function selectAll () {\n      input.value.setSelectionRange(0, input.value.value.length)\n    }\n\n    const copied = ref(false)\n\n    async function copy () {\n      await navigator.clipboard.writeText(url.value)\n      copied.value = true\n      setTimeout(() => {\n        copied.value = false\n      }, 2000)\n    }\n\n    const qrcode = useQrcode(url)\n\n    const share = useShare(() => ({\n      title: props.pkg.name,\n      text: `${props.pkg.name} - ${props.pkg.description}`,\n      url: url.value,\n    }))\n\n    return {\n      close,\n      url,\n      input,\n      selectAll,\n      copy,\n      copied,\n      qrcode,\n      share,\n    }\n  },\n}\n</script>\n\n<template>\n  <PopupModal\n    size=\"small\"\n    @close=\"close()\"\n  >\n    <template #title>\n      Share {{ pkg.name }}\n    </template>\n\n    <div\n      v-if=\"share\"\n      class=\"my-8 flex\"\n    >\n      <BaseButton\n        icon-left=\"share\"\n        class=\"w-full px-8 py-4 bg-yellow-500 hover:bg-yellow-400 text-yellow-900\"\n        @click=\"share()\"\n      >\n        Share\n      </BaseButton>\n    </div>\n\n    <div class=\"my-8 flex\">\n      <input\n        ref=\"input\"\n        :value=\"url\"\n        readonly\n        class=\"bg-black px-8 py-4 rounded w-full flex-1 mr-4\"\n        @focus=\"selectAll()\"\n      >\n\n      <BaseButton\n        :icon-left=\"copied ? 'done' : 'file_copy'\"\n        class=\"px-8 py-4 bg-gray-700 hover:bg-gray-600\"\n        :class=\"{\n          'bg-green-600 hover:bg-green-600 text-green-100': copied,\n        }\"\n        @click=\"!copied && copy()\"\n      >\n        {{ copied ? 'Copied' : 'Copy' }}\n      </BaseButton>\n    </div>\n\n    <div class=\"flex justify-center\">\n      <img\n        :src=\"qrcode\"\n        alt=\"QR Code\"\n        class=\"rounded\"\n      >\n    </div>\n  </PopupModal>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageTabDataSources.vue",
    "content": "<script>\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\n\nimport LoadingIndicator from '../LoadingIndicator.vue'\nimport PackageDataSource from './PackageDataSource.vue'\n\nexport default {\n  components: {\n    LoadingIndicator,\n    PackageDataSource,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { result, loading } = useQuery(() => gql`\n      query PackageDataSources ($id: ID!) {\n        pkg: ${props.pkg.__typename === 'Package' ? 'package' : 'packageProposal'} (id: $id) {\n          id\n          dataSources {\n            type\n            data\n          }\n        }\n      }\n    `, () => ({\n      id: props.pkg.id,\n    }))\n    const dataSources = useResult(result, [], data => data.pkg.dataSources)\n\n    return {\n      loading,\n      dataSources,\n    }\n  },\n}\n</script>\n\n<template>\n  <div>\n    <LoadingIndicator\n      v-if=\"loading\"\n      class=\"p-8\"\n    />\n\n    <div>\n      <PackageDataSource\n        v-for=\"dataSource of dataSources\"\n        :key=\"dataSource.type\"\n        :data-source=\"dataSource\"\n      />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageTabEdit.vue",
    "content": "<script>\nimport LoadingIndicator from '../LoadingIndicator.vue'\nimport PackageEditForm from './PackageEditForm.vue'\nimport PackageEditProjectTypesForm from './PackageEditProjectTypesForm.vue'\n\nimport gql from 'graphql-tag'\nimport { useMutation, useQuery, useResult } from '@vue/apollo-composable'\nimport { pkgFragment } from './fragments'\nimport omit from 'lodash/omit'\nimport { useTags } from '@/util/tags'\n\nexport default {\n  components: {\n    LoadingIndicator,\n    PackageEditForm,\n    PackageEditProjectTypesForm,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { result, loading } = useQuery(gql`\n      query PackageEditInfo ($id: ID!) {\n        package (id: $id) {\n          id\n          info {\n            tags\n          }\n          dataSources {\n            type\n            data\n          }\n          projectTypes {\n            id\n          }\n        }\n      }\n    `, () => ({\n      id: props.pkg.id,\n    }))\n    const pkgEditInfo = useResult(result, { dataSources: [] })\n\n    // Package info\n\n    const { mutate, loading: submitting, error } = useMutation(gql`\n      mutation EditPackageInfo ($input: EditPackageInfoInput!) {\n        editPackageInfo (input: $input) {\n          ...pkg\n          dataSources {\n            type\n            data\n          }\n        }\n      }\n      ${pkgFragment}\n    `)\n\n    async function editPackage (data) {\n      await mutate({\n        input: {\n          common: {\n            id: props.pkg.id,\n            ...data,\n          },\n        },\n      })\n    }\n\n    // Official\n\n    const { isOfficial } = useTags(() => props.pkg)\n    async function toggleOfficial () {\n      const tags = props.pkg.info.tags.slice()\n      if (tags.includes('official')) {\n        tags.splice(tags.indexOf('official'), 1)\n      } else {\n        tags.push('official')\n      }\n\n      await mutate({\n        input: {\n          common: {\n            id: props.pkg.id,\n            info: {\n              ...omit(props.pkg.info, ['__typename']),\n              tags,\n            },\n          },\n        },\n      })\n    }\n\n    // Project types\n\n    const {\n      mutate: mutateProjectTypes,\n      loading: submittingProjectTypes,\n      error: projectTypesError,\n    } = useMutation(gql`\n      mutation EditPackageInfo ($input: EditPackageProjectTypesInput!) {\n        editPackageProjectTypes (input: $input) {\n          id\n          projectTypes {\n            id\n          }\n        }\n      }\n    `)\n\n    async function editProjectTypes (data) {\n      await mutateProjectTypes({\n        input: {\n          packageId: props.pkg.id,\n          ...data,\n        },\n      })\n    }\n\n    return {\n      loading,\n      pkgEditInfo,\n\n      editPackage,\n      submitting,\n      error,\n\n      isOfficial,\n      toggleOfficial,\n\n      editProjectTypes,\n      submittingProjectTypes,\n      projectTypesError,\n    }\n  },\n}\n</script>\n\n<template>\n  <LoadingIndicator\n    v-if=\"loading\"\n    class=\"mt-8\"\n  />\n\n  <div\n    v-else\n    class=\"pb-64\"\n  >\n    <PackageEditForm\n      :pkg=\"pkgEditInfo\"\n      :submitting=\"submitting\"\n      :error=\"error\"\n      @submit=\"editPackage\"\n    >\n      <template #actions>\n        <BaseButton\n          :disabled=\"submitting\"\n          class=\"text-orange-400 bg-yellow-900 hover:bg-yellow-800 px-8 py-4 mr-4\"\n          :icon-left=\"isOfficial ? 'check_box' : 'check_box_outline_blank'\"\n          @click=\"toggleOfficial()\"\n        >\n          Official\n        </BaseButton>\n      </template>\n    </PackageEditForm>\n\n    <hr class=\"border-gray-800 my-8 max-w-3xl\">\n\n    <PackageEditProjectTypesForm\n      :pkg=\"pkgEditInfo\"\n      :submitting=\"submittingProjectTypes\"\n      :error=\"projectTypesError\"\n      @submit=\"editProjectTypes\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageTabGeneral.vue",
    "content": "<script>\nimport PackageLinks from './PackageLinks.vue'\nimport PackageTags from './PackageTags.vue'\nconst PackageReadme = () => import(\n  /* webpackChunkName: \"PackageReadme.vue\" */\n  './PackageReadme.vue'\n)\n\nexport default {\n  components: {\n    PackageLinks,\n    PackageReadme,\n    PackageTags,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n}\n</script>\n\n<template>\n  <div>\n    <PackageLinks\n      :pkg=\"pkg\"\n      class=\"mb-4\"\n    />\n\n    <PackageTags\n      :pkg=\"pkg\"\n    />\n\n    <PackageReadme\n      :package-id=\"pkg.id\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageTabInsight.vue",
    "content": "<script>\nexport default {\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n}\n</script>\n\n<template>\n  <router-view :pkg=\"pkg\" />\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageTabReleases.vue",
    "content": "<script>\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\nimport { releaseFragment } from './fragments'\n\nimport EmptyMessage from '../EmptyMessage.vue'\nimport LoadingIndicator from '../LoadingIndicator.vue'\nimport PackageRelease from './PackageRelease.vue'\n\nexport default {\n  components: {\n    EmptyMessage,\n    LoadingIndicator,\n    PackageRelease,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { result, loading } = useQuery(() => gql`\n      query PackageReleases ($id: ID!) {\n        pkg: ${props.pkg.__typename === 'Package' ? 'package' : 'packageProposal'} (id: $id) {\n          id\n          releases {\n            ...release\n          }\n        }\n      }\n      ${releaseFragment}\n    `, () => ({\n      id: props.pkg.id,\n    }))\n    const releases = useResult(result, [], data => data.pkg.releases)\n\n    return {\n      loading,\n      releases,\n    }\n  },\n}\n</script>\n\n<template>\n  <div>\n    <LoadingIndicator\n      v-if=\"loading\"\n      class=\"p-8\"\n    />\n\n    <div v-else>\n      <PackageRelease\n        v-for=\"release of releases\"\n        :key=\"release.id\"\n        :release=\"release\"\n      />\n\n      <EmptyMessage\n        v-if=\"!releases.length\"\n        icon=\"access_time\"\n      >\n        No releases found\n      </EmptyMessage>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageTag.vue",
    "content": "<script>\nexport default {\n  inheritAttrs: false,\n\n  props: {\n    tag: {\n      type: String,\n      required: true,\n    },\n\n    selected: {\n      type: Boolean,\n      default: false,\n    },\n  },\n}\n</script>\n\n<template>\n  <BaseButton\n    v-bind=\"$attrs\"\n    class=\"px-2 text-gray-500 bg-gray-800 hover:bg-gray-700 rounded flex-none mr-2 mb-2 xl:mb-0\"\n    :class=\"{\n      'text-orange-400 bg-yellow-900 hover:bg-yellow-800': tag === 'official',\n      'text-blue-400 bg-blue-900 hover:bg-blue-800': tag.startsWith('version:'),\n      'text-purple-300 bg-purple-800 hover:bg-purple-700': selected,\n    }\"\n    v-on=\"$listeners\"\n  >\n    <slot>\n      <template v-if=\"tag.startsWith('version:')\">\n        <span>version<span class=\"opacity-50\">:</span></span>\n        <span class=\"font-bold\">{{ tag.substr('version:'.length) }}</span>\n      </template>\n      <template v-else>\n        {{ tag }}\n      </template>\n      <slot name=\"after\" />\n    </slot>\n  </BaseButton>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageTags.vue",
    "content": "<script>\nimport { computed } from '@vue/composition-api'\nimport { useTags } from '@/util/tags'\nimport { isSpecialTag } from '@awesomejs/shared-utils/tags'\n\nimport PackageTag from './PackageTag.vue'\n\nexport default {\n  components: {\n    PackageTag,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { tags } = useTags(() => props.pkg)\n    const sortedTags = computed(() => tags.value.sort((a, b) => {\n      if (isSpecialTag(a)) return -1\n      if (isSpecialTag(b)) return 1\n      return a.localeCompare(b)\n    }))\n\n    return {\n      sortedTags,\n    }\n  },\n}\n</script>\n\n<template>\n  <div class=\"flex flex-wrap\">\n    <i\n      v-tooltip=\"'Tags'\"\n      class=\"material-icons text-gray-600 mr-2\"\n    >filter_list</i>\n    <PackageTag\n      v-for=\"tag of sortedTags\"\n      :key=\"tag\"\n      :tag=\"tag\"\n    />\n    <div\n      v-if=\"!pkg.info.tags.length\"\n      class=\"text-gray-700\"\n    >\n      No tags\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageView.vue",
    "content": "<script>\nimport PackageViewLayout from './PackageViewLayout.vue'\nimport PackageBookmarkButton from './PackageBookmarkButton.vue'\nimport PackageReleaseCount from './PackageReleaseCount.vue'\nimport PackageShareButton from './PackageShareButton.vue'\nimport PackageInstallButton from './PackageInstallButton.vue'\nimport RouteTab from '../RouteTab.vue'\n\nimport gql from 'graphql-tag'\nimport { pkgFragment } from './fragments'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { computed } from '@vue/composition-api'\nimport { installationAvailable } from '@/util/installation'\n\nexport default {\n  components: {\n    PackageViewLayout,\n    PackageBookmarkButton,\n    PackageReleaseCount,\n    PackageShareButton,\n    PackageInstallButton,\n    RouteTab,\n  },\n\n  props: {\n    packageId: {\n      type: String,\n      required: true,\n    },\n\n    routePrefix: {\n      type: String,\n      required: true,\n    },\n  },\n\n  setup (props, { root }) {\n    const { result, loading } = useQuery(gql`\n      query Package ($packageId: ID!) {\n        pkg: package (id: $packageId) {\n          ...pkg\n        }\n      }\n      ${pkgFragment}\n    `, props)\n    const pkg = useResult(result)\n\n    const { result: additionalResult } = useQuery(gql`\n      query PackageProjectTypes ($packageId: ID!) {\n        pkg: package (id: $packageId) {\n          id\n          projectTypes {\n            id\n            inTeam\n          }\n        }\n      }\n    `, props)\n    const inTeam = computed(() => {\n      if (additionalResult.value) {\n        return additionalResult.value.pkg.projectTypes.some(pt => pt.inTeam)\n      }\n    })\n\n    return {\n      pkg,\n      loading,\n      inTeam,\n      installationAvailable,\n    }\n  },\n\n  metaInfo () {\n    if (!this.pkg) return\n\n    return {\n      title: this.pkg.name,\n      meta: [\n        {\n          property: 'og:title',\n          content: this.pkg.name,\n          vmid: 'og:title',\n        },\n        {\n          property: 'og:description',\n          content: this.pkg.description,\n          vmid: 'og:description',\n        },\n      ],\n    }\n  },\n}\n</script>\n\n<template>\n  <PackageViewLayout\n    :pkg=\"pkg\"\n    :loading=\"loading\"\n  >\n    <template #actions>\n      <PackageInstallButton\n        v-if=\"installationAvailable\"\n        :pkg=\"pkg\"\n        class=\"mr-4\"\n      />\n\n      <PackageBookmarkButton\n        :package-id=\"pkg.id\"\n        class=\"mr-4\"\n      />\n\n      <PackageShareButton\n        :pkg=\"pkg\"\n      />\n    </template>\n\n    <template #tabs>\n      <RouteTab\n        :to=\"{ name: `${routePrefix}package` }\"\n        class=\"flex-none\"\n        exact\n      >\n        General\n      </RouteTab>\n\n      <RouteTab\n        :to=\"{ name: `${routePrefix}package-releases` }\"\n        class=\"flex-none\"\n      >\n        Releases\n        <PackageReleaseCount\n          v-if=\"!loading\"\n          :pkg=\"pkg\"\n        />\n      </RouteTab>\n\n      <RouteTab\n        :to=\"{ name: `${routePrefix}package-insight` }\"\n        class=\"flex-none\"\n      >\n        Insight\n      </RouteTab>\n\n      <RouteTab\n        :to=\"{ name: `${routePrefix}package-data-sources` }\"\n        class=\"flex-none\"\n      >\n        Data sources\n      </RouteTab>\n\n      <RouteTab\n        v-if=\"inTeam\"\n        :to=\"{ name: `${routePrefix}package-edit` }\"\n        class=\"flex-none\"\n      >\n        Edit\n      </RouteTab>\n    </template>\n  </PackageViewLayout>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/PackageViewLayout.vue",
    "content": "<script>\nimport { ref, watch } from '@vue/composition-api'\n\nimport ErrorMessage from '../ErrorMessage.vue'\nimport LoadingIndicator from '../LoadingIndicator.vue'\nimport PackageDownloadsCount from './PackageDownloadsCount.vue'\nimport PackageGeneralInfo from './PackageGeneralInfo.vue'\n\nexport default {\n  components: {\n    ErrorMessage,\n    LoadingIndicator,\n    PackageDownloadsCount,\n    PackageGeneralInfo,\n  },\n\n  props: {\n    pkg: {\n      type: Object,\n      default: null,\n    },\n\n    loading: {\n      type: Boolean,\n      default: false,\n    },\n\n    error: {\n      type: String,\n      default: null,\n    },\n  },\n\n  setup (props, { root }) {\n    const scrollMarker = ref()\n    watch(() => root.$route, () => {\n      if (scrollMarker.value && scrollMarker.value.getBoundingClientRect().y < 0) {\n        scrollMarker.value.scrollIntoView()\n      }\n    })\n\n    return {\n      scrollMarker,\n    }\n  },\n}\n</script>\n\n<template>\n  <div>\n    <template v-if=\"loading\">\n      <!-- General info -->\n      <div\n        class=\"flex items-center pb-4 lg:py-4\"\n      >\n        <div\n          v-if=\"!$responsive.sm\"\n          class=\"mr-6 w-10 h-10 bg-gray-600 rounded\"\n        />\n\n        <div class=\"flex-1 w-0 mr-6 overflow-hidden\">\n          <div class=\"flex my-2\">\n            <span class=\"w-24 h-3 bg-gray-100 rounded opacity-50 inline-block mr-3\" />\n            <span class=\"w-16 h-3 bg-gray-600 rounded opacity-50 inline-block\" />\n          </div>\n\n          <div class=\"w-40 h-3 my-2 bg-gray-500 rounded opacity-50\" />\n        </div>\n\n        <div class=\"w-32 h-3 bg-purple-500 rounded opacity-50\" />\n      </div>\n\n      <div class=\"mb-4\">\n        <div class=\"bg-gray-800 px-8 py-4 w-48 rounded text-transparent\">\n          Button\n        </div>\n      </div>\n\n      <div ref=\"scrollMarker\" />\n\n      <div class=\"overflow-x-auto flex pb-4 lg:sticky lg:top-0 lg:bg-gray-900 lg:z-10\">\n        <slot name=\"tabs\" />\n      </div>\n\n      <LoadingIndicator class=\"my-16\" />\n    </template>\n    <template v-else>\n      <PackageGeneralInfo\n        :pkg=\"pkg\"\n      >\n        <PackageDownloadsCount\n          :pkg=\"pkg\"\n          class=\"mr-4\"\n        />\n      </PackageGeneralInfo>\n\n      <div class=\"mb-4 flex overflow-x-auto\">\n        <slot name=\"actions\" />\n      </div>\n\n      <div ref=\"scrollMarker\" />\n\n      <div class=\"overflow-x-auto flex pb-4 lg:sticky lg:top-0 lg:bg-gray-900 lg:z-10\">\n        <slot name=\"tabs\" />\n      </div>\n\n      <ErrorMessage\n        v-if=\"error\"\n        :error=\"error\"\n        class=\"error-box my-4\"\n      />\n\n      <div>\n        <router-view :pkg=\"pkg\" />\n      </div>\n    </template>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/pkg/fragments.js",
    "content": "import gql from 'graphql-tag'\n\nexport const pkgFragment = gql`\nfragment pkg on Package {\n  id\n  name\n  description\n  stars\n  defaultLogo\n  homepage\n  repo\n  maintainers {\n    name\n    email\n    avatar\n  }\n  info {\n    tags\n  }\n}\n`\n\nexport const pkgProposalFragment = gql`\nfragment pkgProposal on PackageProposal {\n  id\n  name\n  description\n  upvotes\n  stars\n  defaultLogo\n  homepage\n  repo\n  maintainers {\n    name\n    email\n    avatar\n  }\n  info {\n    tags\n  }\n}\n`\n\nexport const releaseFragment = gql`\nfragment release on PackageRelease {\n  id\n  date\n  title\n  tagName\n  description\n  prerelease\n  assets {\n    name\n    downloadUrl\n    size\n  }\n}\n`\n"
  },
  {
    "path": "packages/frontend/src/components/project-type/ProjectTypeAllTags.vue",
    "content": "<script>\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\nimport { computed, ref, watch } from '@vue/composition-api'\n\nimport LoadingIndicator from '../LoadingIndicator.vue'\nimport PackageTag from '../pkg/PackageTag.vue'\n\nexport default {\n  components: {\n    LoadingIndicator,\n    PackageTag,\n  },\n\n  props: {\n    projectTypeId: {\n      type: String,\n      required: true,\n    },\n\n    selectedTags: {\n      type: Array,\n      default: () => [],\n    },\n  },\n\n  setup (props) {\n    const isOpen = ref(false)\n\n    const { result, loading } = useQuery(gql`\n      query ProjectTypeAllTags ($id: ID!) {\n        projectType (id: $id) {\n          id\n          popularTags {\n            id\n            count\n          }\n          tags {\n            id\n            count\n          }\n        }\n      }\n    `, () => ({\n      id: props.projectTypeId,\n    }), () => ({\n      enabled: !!isOpen.value,\n    }))\n    const tags = useResult(result, [], data => data.projectType.tags)\n\n    const popularTags = useResult(result, [], data => data.projectType.popularTags)\n    const otherSelectedTags = computed(() => props.selectedTags.filter(id => !popularTags.value.find(t => t.id === id)))\n\n    const searchText = ref('')\n    const filteredTags = computed(() => {\n      if (searchText.value) {\n        const reg = new RegExp(searchText.value.trim().replace(/\\s+/g, '|'), 'i')\n        return tags.value.filter(t => t.id.match(reg))\n      } else {\n        return tags.value\n      }\n    })\n\n    const searchInput = ref('')\n    watch(isOpen, value => {\n      if (value) {\n        setTimeout(() => {\n          searchInput.value.focus()\n        }, 150)\n      }\n    })\n\n    return {\n      isOpen,\n      filteredTags,\n      popularTags,\n      otherSelectedTags,\n      loading,\n      searchText,\n      searchInput,\n    }\n  },\n}\n</script>\n\n<template>\n  <BasePopper\n    placement=\"bottom\"\n    @update:open=\"value => isOpen = value\"\n  >\n    <BaseButton\n      v-tooltip=\"'See all tags'\"\n    >\n      <PackageTag\n        tag=\"...\"\n        :class=\"{\n          'text-purple-300 bg-purple-800 hover:bg-purple-700': otherSelectedTags.length,\n        }\"\n      >\n        <i class=\"material-icons\">more_horiz</i>\n      </PackageTag>\n    </BaseButton>\n\n    <template #popper>\n      <div class=\"p-2\">\n        <input\n          ref=\"searchInput\"\n          v-model=\"searchText\"\n          placeholder=\"Search...\"\n          maxlength=\"200\"\n          class=\"bg-black px-4 py-2 rounded w-full mb-2\"\n        >\n\n        <div class=\"scroller w-64 flex flex-col items-stretch overflow-y-scroll\">\n          <LoadingIndicator\n            v-if=\"loading\"\n            class=\"flex-1\"\n          />\n          <template v-else>\n            <BaseButton\n              v-for=\"tag of filteredTags\"\n              :key=\"tag.id\"\n              class=\"px-4 py-2 text-gray-500 hover:bg-gray-800 rounded flex-none mb-2\"\n              :class=\"{\n                'text-purple-300 bg-purple-800 hover:bg-purple-700': selectedTags.includes(tag.id),\n              }\"\n              @click=\"$emit('select', tag.id)\"\n            >\n              <div class=\"flex items-start w-full\">\n                <span class=\"flex-1 text-left truncate mr-2\">{{ tag.id }}</span>\n                <span class=\"opacity-50\">{{ tag.count }}</span>\n              </div>\n            </BaseButton>\n          </template>\n        </div>\n      </div>\n    </template>\n  </BasePopper>\n</template>\n\n<style lang=\"postcss\" scoped>\n.scroller {\n  min-height: 200px;\n  max-height: 400px;\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/project-type/ProjectTypeBookmarkButton.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { useMutation } from '@vue/apollo-composable'\n\nexport default {\n  props: {\n    projectType: {\n      type: Object,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { mutate: toggle } = useMutation(gql`\n      mutation ToggleProjectTypeBookmark ($input: ToggleProjectTypeBookmarkInput!) {\n        toggleProjectTypeBookmark (input: $input) {\n          id\n          bookmarked\n        }\n      }\n    `, () => ({\n      variables: {\n        input: {\n          projectTypeId: props.projectType.id,\n        },\n      },\n      optimisticResponse: {\n        __typename: 'Mutation',\n        toggleProjectTypeBookmark: {\n          __typename: 'ProjectType',\n          id: props.projectType.id,\n          bookmarked: !props.projectType.bookmarked,\n        },\n      },\n    }))\n\n    return {\n      toggle,\n    }\n  },\n}\n</script>\n\n<template>\n  <BaseButton\n    v-tooltip=\"projectType.bookmarked ? 'Remove from bookmarks' : 'Add to bookmarks'\"\n    class=\"p-1 text-gray-600 hover:text-gray-500 text-xl lg:text-2xl\"\n    @click=\"toggle()\"\n  >\n    <i class=\"material-icons\">\n      {{ projectType.bookmarked ? 'bookmark' : 'bookmark_border' }}\n    </i>\n  </BaseButton>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/project-type/ProjectTypeMultiSelect.vue",
    "content": "<script>\nimport MultiSelect from 'vue-multiselect'\nimport gql from 'graphql-tag'\nimport { computed } from '@vue/composition-api'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { projectTypeFragment } from './fragments'\n\nexport default {\n  components: {\n    MultiSelect,\n  },\n\n  model: {\n    prop: 'projectTypeIds',\n    event: 'update',\n  },\n\n  props: {\n    projectTypeIds: {\n      type: Array,\n      required: true,\n    },\n  },\n\n  setup (props, { emit }) {\n    // Project types\n    const { result } = useQuery(gql`\n      query ProjectTypes {\n        projectTypes {\n          ...projectType\n        }\n      }\n      ${projectTypeFragment}\n    `)\n    const projectTypes = useResult(result, [])\n\n    const model = computed({\n      get: () => projectTypes.value.filter(pt => props.projectTypeIds.includes(pt.id)),\n      set: (value) => {\n        emit('update', value.map(pt => pt.id))\n      },\n    })\n\n    return {\n      projectTypes,\n      model,\n    }\n  },\n}\n</script>\n\n<template>\n  <MultiSelect\n    v-model=\"model\"\n    :options=\"projectTypes\"\n    :close-on-select=\"false\"\n    :option-height=\"56\"\n    label=\"name\"\n    track-by=\"id\"\n    placeholder=\"Select project types...\"\n    multiple\n  >\n    <template #option=\"{ option }\">\n      <div class=\"flex items-center\">\n        <img\n          :src=\"option.logo\"\n          alt=\"logo\"\n          class=\"w-8 h-18 mr-4\"\n        >\n\n        <div>\n          {{ option.name }}\n        </div>\n      </div>\n    </template>\n  </MultiSelect>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/project-type/ProjectTypePackageProposalsButton.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { useQuery, useResult } from '@vue/apollo-composable'\n\nexport default {\n  props: {\n    projectTypeId: {\n      type: String,\n      required: true,\n    },\n  },\n\n  setup (props) {\n    const { result, loading } = useQuery(gql`\n      query ProjectTypePackageProposalCount ($id: ID!) {\n        projectType (id: $id) {\n          id\n          slug\n          packageProposalCount\n        }\n      }\n    `, () => ({\n      id: props.projectTypeId,\n    }), {\n      fetchPolicy: 'cache-and-network',\n    })\n    const projectType = useResult(result)\n\n    return {\n      projectType,\n      loading,\n    }\n  },\n}\n</script>\n\n<template>\n  <BaseButton\n    :to=\"{\n      name: 'project-type-proposals',\n      params: {\n        projectTypeSlug: projectType && projectType.slug\n      }\n    }\"\n    icon-left=\"thumb_up\"\n    class=\"flex items-center bg-gray-800 text-purple-300 rounded px-6 py-4 hover:bg-gray-700\"\n  >\n    <template v-if=\"loading && !projectType\">\n      Counting proposed packages\n    </template>\n    <template v-else-if=\"projectType.packageProposalCount\">\n      <b class=\"font-bold ml-2 mr-1\">{{ projectType.packageProposalCount }}</b>\n      package{{ projectType.packageProposalCount > 1 ? 's' : '' }} proposed\n    </template>\n    <template v-else>\n      No package proposed yet\n    </template>\n  </BaseButton>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/project-type/ProjectTypePackageProposalsView.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { projectTypeFragment } from './fragments'\nimport { useCurrentUser } from '../user/useCurrentUser'\n\nimport PageTitle from '../PageTitle.vue'\nimport PackageProposalList from '../pkg/PackageProposalList.vue'\n\nexport default {\n  components: {\n    PageTitle,\n    PackageProposalList,\n  },\n\n  props: {\n    projectTypeSlug: {\n      type: String,\n      required: true,\n    },\n\n    packageId: {\n      type: String,\n      default: null,\n    },\n  },\n\n  setup (props) {\n    const { result } = useQuery(gql`\n      query ProjectType ($slug: String!) {\n        projectType: projectTypeBySlug (slug: $slug) {\n          ...projectType\n        }\n      }\n      ${projectTypeFragment}\n    `, () => ({\n      slug: props.projectTypeSlug,\n    }))\n    const projectType = useResult(result)\n\n    // Admin\n    const { isAdmin } = useCurrentUser()\n\n    return {\n      projectType,\n      isAdmin,\n    }\n  },\n\n  metaInfo () {\n    if (!this.projectType) return\n\n    return {\n      title: `Awesome ${this.projectType.name} proposals`,\n    }\n  },\n}\n</script>\n\n<template>\n  <div v-if=\"projectType\">\n    <PageTitle\n      v-if=\"!$responsive.lg || !packageId\"\n      :back-to=\"{\n        name: 'project-type',\n        params: {\n          projectTypeSlug,\n        },\n      }\"\n      class=\"mb-8\"\n    >\n      Proposed {{ projectType.name }} packages\n\n      <template #after>\n        <div\n          v-if=\"!isAdmin && projectType.inTeam\"\n          v-tooltip=\"'You have moderation rights on this project type'\"\n          class=\"text-orange-300 bg-orange-700 p-1 rounded ml-4 text-sm leading-none\"\n        >\n          <i class=\"material-icons text-base\">supervisor_account</i>\n          Team\n        </div>\n      </template>\n    </PageTitle>\n\n    <div class=\"flex\">\n      <div\n        v-if=\"!$responsive.lg || !packageId\"\n        class=\"w-full lg:w-1/3 lg:pb-64 lg:mt-4 lg:sticky lg:top-4 lg:max-h-screen lg:overflow-y-auto\"\n        :class=\"{\n          'scroll-parent': !$responsive.lg,\n        }\"\n      >\n        <PackageProposalList\n          :project-type-id=\"projectType.id\"\n        />\n      </div>\n\n      <div\n        v-if=\"!$responsive.lg || packageId\"\n        class=\"w-full lg:w-2/3 lg:pl-16\"\n      >\n        <router-view\n          :project-type-id=\"projectType.id\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/project-type/ProjectTypeSelect.vue",
    "content": "<script>\nimport ProjectTypesItem from './ProjectTypesItem.vue'\nimport PopupModal from '../PopupModal.vue'\n\nimport gql from 'graphql-tag'\nimport { ref, computed } from '@vue/composition-api'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { projectTypeFragment } from './fragments'\n\nexport default {\n  components: {\n    PopupModal,\n    ProjectTypesItem,\n  },\n\n  model: {\n    prop: 'projectTypeId',\n    event: 'update',\n  },\n\n  props: {\n    projectTypeId: {\n      type: String,\n      default: null,\n    },\n\n    projectTypeSlug: {\n      type: String,\n      default: null,\n    },\n\n    placeholder: {\n      type: String,\n      default: 'Select a project type...',\n    },\n\n    buttonClass: {\n      type: [String, Array, Object],\n      default: 'bg-gray-800 hover:bg-gray-700 px-8 py-4',\n    },\n  },\n\n  setup (props, { emit }) {\n    const open = ref(false)\n\n    // Project types\n    const { result } = useQuery(gql`\n      query ProjectTypes {\n        projectTypes {\n          ...projectType\n        }\n      }\n      ${projectTypeFragment}\n    `)\n    const projectTypes = useResult(result, [])\n\n    // Search\n    const searchText = ref('')\n    const filteredProjectTypes = computed(() => {\n      if (searchText.value) {\n        const reg = new RegExp(searchText.value.trim(), 'i')\n        return projectTypes.value.filter(p => reg.test(p.name))\n      }\n      return projectTypes.value\n    })\n\n    // Current\n    const currentProjectType = computed(() => projectTypes.value.find(\n      p => p.id === props.projectTypeId || p.slug === props.projectTypeSlug\n    ))\n\n    // Select\n    function select (selectedProjectType) {\n      if (selectedProjectType === currentProjectType.value) {\n        emit('update', null)\n        emit('update:projectTypeSlug', null)\n      } else {\n        emit('update', selectedProjectType.id)\n        emit('update:projectTypeSlug', selectedProjectType.slug)\n      }\n      this.open = false\n    }\n\n    return {\n      open,\n\n      currentProjectType,\n\n      searchText,\n      filteredProjectTypes,\n\n      select,\n    }\n  },\n}\n</script>\n\n<template>\n  <div>\n    <BaseButton\n      class=\"inline-block w-full\"\n      :class=\"buttonClass\"\n      @click=\"open = true\"\n    >\n      <span v-if=\"!currentProjectType\">\n        {{ placeholder }}\n      </span>\n      <span\n        v-else\n        class=\"flex items-center w-full\"\n      >\n        <img\n          :src=\"currentProjectType.logo\"\n          :alt=\"`${currentProjectType.name} logo`\"\n          class=\"w-6 h-6 mr-4\"\n        >\n        <span class=\"flex-1 truncate\">\n          {{ currentProjectType.name }}\n        </span>\n      </span>\n    </BaseButton>\n\n    <PopupModal\n      v-if=\"open\"\n      @close=\"open = false\"\n    >\n      <template #title>\n        Select project type\n      </template>\n\n      <div class=\"flex justify-center mb-2 lg:mb-8\">\n        <input\n          ref=\"input\"\n          v-model=\"searchText\"\n          v-focus.lazy=\"true\"\n          placeholder=\"Search...\"\n          class=\"bg-gray-900 px-4 py-2 rounded w-full sm:w-64\"\n          @keyup.enter=\"!$responsive.small && filteredProjectTypes.length && select(filteredProjectTypes[0])\"\n        >\n      </div>\n\n      <div class=\"project-types-grid\">\n        <BaseButton\n          v-for=\"p of filteredProjectTypes\"\n          :key=\"p.id\"\n          @click=\"select(p)\"\n        >\n          <ProjectTypesItem\n            :project-type=\"p\"\n            :selected=\"p === currentProjectType\"\n          />\n        </BaseButton>\n      </div>\n\n      <div class=\"h-32 sm:hidden\" />\n    </PopupModal>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/project-type/ProjectTypeView.vue",
    "content": "<script>\nimport gql from 'graphql-tag'\nimport { watch, ref, onUnmounted } from '@vue/composition-api'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { projectTypeFragment } from './fragments'\nimport { setFavicon, resetFavicon } from '@/util/favicon'\nimport { useCurrentUser } from '../user/useCurrentUser'\n\nimport PageTitle from '../PageTitle.vue'\nimport PackageList from '../pkg/PackageList.vue'\nimport PackageTag from '../pkg/PackageTag.vue'\nimport ProjectTypeAllTags from './ProjectTypeAllTags.vue'\nimport ProjectTypeBookmarkButton from './ProjectTypeBookmarkButton.vue'\nimport ProjectTypePackageProposalsButton from './ProjectTypePackageProposalsButton.vue'\n\nexport default {\n  components: {\n    PageTitle,\n    PackageList,\n    PackageTag,\n    ProjectTypeAllTags,\n    ProjectTypeBookmarkButton,\n    ProjectTypePackageProposalsButton,\n  },\n\n  props: {\n    projectTypeSlug: {\n      type: String,\n      required: true,\n    },\n\n    packageId: {\n      type: String,\n      default: null,\n    },\n  },\n\n  setup (props) {\n    const { result } = useQuery(gql`\n      query ProjectTypeAndPopularTags ($slug: String!) {\n        projectType: projectTypeBySlug (slug: $slug) {\n          ...projectType\n          popularTags {\n            id\n            count\n          }\n        }\n      }\n      ${projectTypeFragment}\n    `, () => ({\n      slug: props.projectTypeSlug,\n    }))\n    const projectType = useResult(result)\n\n    // Favicon\n    watch(projectType, value => {\n      if (value) {\n        setFavicon(value.logo)\n      }\n    })\n    onUnmounted(() => {\n      resetFavicon()\n    })\n\n    // Tags\n    const tags = useResult(result, [], data => data.projectType.popularTags)\n    const selectedTags = ref([])\n    function toggleTag (tag) {\n      const index = selectedTags.value.indexOf(tag)\n      if (index === -1) {\n        selectedTags.value.push(tag)\n      } else {\n        selectedTags.value.splice(index, 1)\n      }\n    }\n\n    // Scroll to top\n    const scroller = ref()\n    watch(() => props.packageId, () => {\n      scroller.value && (scroller.value.scrollTop = 0)\n    })\n\n    // Admin\n    const { isAdmin } = useCurrentUser()\n\n    return {\n      projectType,\n\n      tags,\n      selectedTags,\n      toggleTag,\n\n      scroller,\n\n      isAdmin,\n    }\n  },\n\n  metaInfo () {\n    if (!this.projectType) return\n\n    return {\n      title: `Awesome ${this.projectType.name} packages`,\n    }\n  },\n}\n</script>\n\n<template>\n  <div v-if=\"projectType\">\n    <template v-if=\"!$responsive.lg || !packageId\">\n      <PageTitle\n        :back-to=\"{ name: 'home' }\"\n        class=\"mb-4\"\n      >\n        <span v-if=\"!$responsive.sm\">Awesome</span> {{ projectType.name }} packages\n\n        <template #after>\n          <ProjectTypeBookmarkButton\n            :project-type=\"projectType\"\n          />\n\n          <div\n            v-if=\"!isAdmin && projectType.inTeam\"\n            v-tooltip=\"'You have moderation rights on this project type'\"\n            class=\"text-orange-300 bg-orange-700 p-1 rounded ml-4 text-sm leading-none\"\n          >\n            <i class=\"material-icons text-base\">supervisor_account</i>\n            Team\n          </div>\n        </template>\n      </PageTitle>\n\n      <div\n        v-if=\"tags.length\"\n        class=\"my-2 xl:mt-0 lg:my-4 xl:mb-4 flex flex-wrap justify-stretch -mr-2\"\n      >\n        <i class=\"material-icons text-gray-600 mr-2 text-xl flex-none\">filter_list</i>\n        <PackageTag\n          v-for=\"tag of tags\"\n          :key=\"tag.id\"\n          v-tooltip=\"`${tag.count} package${tag.count > 1 ? 's' : ''}`\"\n          :tag=\"tag.id\"\n          :selected=\"selectedTags.includes(tag.id)\"\n          @click=\"toggleTag(tag.id)\"\n        />\n\n        <ProjectTypeAllTags\n          :project-type-id=\"projectType.id\"\n          :selected-tags=\"selectedTags\"\n          @select=\"toggleTag\"\n        />\n      </div>\n    </template>\n\n    <div class=\"flex\">\n      <div\n        v-if=\"!$responsive.lg || !packageId\"\n        class=\"w-full lg:w-1/3 lg:pb-64 lg:mt-4 lg:sticky lg:top-4 lg:max-h-screen lg:overflow-y-auto\"\n        :class=\"{\n          'scroll-parent': !$responsive.lg,\n        }\"\n      >\n        <ProjectTypePackageProposalsButton\n          :project-type-id=\"projectType.id\"\n          class=\"w-full mb-4 sm:mb-6\"\n        />\n\n        <PackageList\n          :project-type-slug=\"projectType.slug\"\n          :tags=\"selectedTags\"\n        />\n      </div>\n\n      <div\n        v-if=\"!$responsive.lg || packageId\"\n        ref=\"scroller\"\n        class=\"w-full lg:w-2/3 lg:pl-16\"\n      >\n        <router-view\n          :project-type-id=\"projectType.id\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/project-type/ProjectTypes.vue",
    "content": "<script>\nimport LoadingIndicator from '../LoadingIndicator.vue'\nimport ProjectTypesGrid from './ProjectTypesGrid.vue'\n\nimport gql from 'graphql-tag'\nimport { computed } from '@vue/composition-api'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { projectTypeFragment } from './fragments'\n\nexport default {\n  components: {\n    LoadingIndicator,\n    ProjectTypesGrid,\n  },\n\n  setup () {\n    const { result, loading } = useQuery(gql`\n      query ProjectTypes {\n        projectTypes {\n          ...projectType\n        }\n      }\n      ${projectTypeFragment}\n    `)\n    const projectTypes = useResult(result, [])\n\n    const bookmarkedProjectTypes = computed(() => projectTypes.value.filter(pt => pt.bookmarked))\n    const otherProjectTypes = computed(() => projectTypes.value.filter(pt => !pt.bookmarked))\n\n    return {\n      loading,\n      bookmarkedProjectTypes,\n      otherProjectTypes,\n    }\n  },\n}\n</script>\n\n<template>\n  <LoadingIndicator\n    v-if=\"loading\"\n    class=\"py-16\"\n  />\n  <div v-else>\n    <template v-if=\"bookmarkedProjectTypes.length\">\n      <h2 class=\"mt-8 mb-4 text-gray-600 sm:text-xl\">\n        <i class=\"material-icons text-lg sm:text-2xl\">bookmark</i>\n        Bookmarked\n      </h2>\n\n      <ProjectTypesGrid\n        :project-types=\"bookmarkedProjectTypes\"\n        class=\"mb-8\"\n      />\n    </template>\n\n    <hr\n      v-if=\"bookmarkedProjectTypes.length && otherProjectTypes.length\"\n      class=\"border-gray-800\"\n    >\n\n    <ProjectTypesGrid\n      v-if=\"otherProjectTypes.length\"\n      :project-types=\"otherProjectTypes\"\n      class=\"my-8\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/project-type/ProjectTypesGrid.vue",
    "content": "<script>\nimport ProjectTypesItem from './ProjectTypesItem.vue'\n\nexport default {\n  components: {\n    ProjectTypesItem,\n  },\n\n  props: {\n    projectTypes: {\n      type: Array,\n      required: true,\n    },\n  },\n}\n</script>\n\n<template>\n  <div\n    class=\"project-types-grid\"\n  >\n    <router-link\n      v-for=\"projectType of projectTypes\"\n      :key=\"projectType.id\"\n      :to=\"{\n        name: 'project-type',\n        params: {\n          projectTypeSlug: projectType.slug\n        }\n      }\"\n    >\n      <ProjectTypesItem\n        :project-type=\"projectType\"\n      />\n    </router-link>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/project-type/ProjectTypesItem.vue",
    "content": "<script>\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\nimport { useCurrentUser } from '../user/useCurrentUser'\n\nexport default {\n  props: {\n    projectType: {\n      type: Object,\n      required: true,\n    },\n\n    selected: {\n      type: Boolean,\n      default: false,\n    },\n  },\n\n  setup (props) {\n    const { isAdmin } = useCurrentUser()\n\n    const { result } = useQuery(gql`\n      query ProjectTypePackageProposalCount ($id: ID!) {\n        projectType (id: $id) {\n          id\n          slug\n          packageProposalCount\n        }\n      }\n    `, () => ({\n      id: props.projectType.id,\n    }), () => ({\n      fetchPolicy: 'cache-and-network',\n      enabled: props.projectType.inTeam,\n    }))\n    const packageProposalCount = useResult(result, 0, data => data.projectType.packageProposalCount)\n\n    return {\n      packageProposalCount,\n      isAdmin,\n    }\n  },\n}\n</script>\n\n<template>\n  <div\n    class=\"\n    flex flex-col items-center justify-center p-2 bg-gray-800 rounded\n    w-24 h-24 lg:w-32 lg:h-32 cursor-pointer relative\n    hover:bg-gray-700\n    \"\n    :class=\"{\n      'text-orange-200 bg-orange-900 hover:bg-orange-800': !isAdmin && projectType.inTeam,\n      'text-purple-200 bg-purple-800 hover:bg-purple-700': selected,\n    }\"\n  >\n    <div\n      class=\"w-8 lg:w-16 flex items-center justify-center flex-1\"\n    >\n      <img\n        :src=\"projectType.logo\"\n        :alt=\"`${projectType.name} logo`\"\n        class=\"max-w-full max-h-full rounded\"\n      >\n    </div>\n\n    <div class=\"truncate max-w-full\">\n      {{ projectType.name }}\n    </div>\n\n    <div\n      v-if=\"packageProposalCount\"\n      v-tooltip=\"'Proposed packages'\"\n      class=\"absolute top-0 left-0 pt-1 pl-1\"\n    >\n      <div class=\"text-xs text-orange-100 bg-orange-700 px-1 rounded\">\n        {{ packageProposalCount }}\n      </div>\n    </div>\n\n    <div\n      v-if=\"!isAdmin && projectType.inTeam\"\n      v-tooltip=\"'You have moderation rights on this project type'\"\n      class=\"absolute top-0 right-0 pt-1 pr-1\"\n    >\n      <div class=\"text-orange-300 bg-orange-700 px-1 rounded leading-none\">\n        <i class=\"material-icons text-base\">supervisor_account</i>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/project-type/fragments.js",
    "content": "import gql from 'graphql-tag'\n\nexport const projectTypeFragment = gql`\nfragment projectType on ProjectType {\n  id\n  name\n  slug\n  logo\n  bookmarked\n  inTeam\n}\n`\n"
  },
  {
    "path": "packages/frontend/src/components/search/SearchOverlay.vue",
    "content": "<script>\nimport EmptyMessage from '../EmptyMessage.vue'\nimport LoadingIndicator from '../LoadingIndicator.vue'\nimport PackageListItem from '../pkg/PackageListItem.vue'\nimport ProjectTypeSelect from '../project-type/ProjectTypeSelect.vue'\n\nimport { FocusTrap } from 'focus-trap-vue'\nimport { ref, computed, watch, onActivated } from '@vue/composition-api'\nimport { useSearch } from '@/util/algolia'\nimport { useNpmSearch } from '@/util/algolia-npm'\nimport { useLockScroll } from '@/util/lock-scroll'\nimport { useRouter, useRoute } from '@/util/router'\n\nexport default {\n  components: {\n    EmptyMessage,\n    FocusTrap,\n    LoadingIndicator,\n    PackageListItem,\n    ProjectTypeSelect,\n  },\n\n  setup (props, { emit }) {\n    const router = useRouter()\n    const currentRoute = useRoute()\n\n    // Close\n    function close () {\n      emit('close')\n    }\n    watch(currentRoute, () => close(), {\n      lazy: true,\n    })\n\n    // Input\n\n    const input = ref(null)\n\n    function focusInput () {\n      if (input.value) {\n        input.value.focus()\n        input.value.select()\n      }\n    }\n\n    // Filters\n\n    // Project type filter\n    const projectTypeId = ref(null)\n    const projectTypeSlug = ref(null)\n    watch(projectTypeSlug, () => focusInput())\n\n    const hasFilters = computed(\n      () => !!searchText.value || projectTypeSlug.value != null\n    )\n\n    function clearFilters () {\n      searchText.value = ''\n      projectTypeId.value = null\n      projectTypeSlug.value = null\n    }\n\n    // Algolia\n\n    const searchParams = computed(() => ({\n      facetFilters: [\n        ...projectTypeSlug.value ? [`projectTypes.slug:${projectTypeSlug.value}`] : [],\n      ],\n    }))\n\n    const { searchText, result } = useSearch('packages', searchParams)\n\n    // Search results\n\n    function getRoute (hit) {\n      return {\n        name: 'package',\n        params: {\n          projectTypeSlug: hit.projectTypes[0].slug,\n          packageId: hit.id,\n        },\n      }\n    }\n\n    const list = ref(null)\n    const focusIndex = ref(0)\n\n    watch(result, () => {\n      focusIndex.value = 0\n    })\n\n    function focusPrevious () {\n      if (focusIndex.value > 0) {\n        focusIndex.value--\n        scrollToFocused()\n      }\n    }\n\n    function focusNext () {\n      if (focusIndex.value < result.value.hits.length - 1) {\n        focusIndex.value++\n        scrollToFocused()\n      }\n    }\n\n    function scrollToFocused () {\n      const el = list.value.querySelector('.focused')\n      el.scrollIntoViewIfNeeded ? el.scrollIntoViewIfNeeded() : el.scrollIntoView()\n    }\n\n    function selectFocused () {\n      if (focusIndex.value < result.value.hits.length) {\n        router.push(getRoute(result.value.hits[focusIndex.value]))\n      }\n    }\n\n    useLockScroll()\n\n    // Open overlay\n\n    function onOpen () {\n      projectTypeSlug.value = currentRoute.value.params.projectTypeSlug\n      focusInput()\n    }\n\n    onOpen()\n    onActivated(onOpen)\n\n    // Npm Search\n    const { searchText: npmSearchText, result: npmSearchResult } = useNpmSearch()\n\n    watch(searchText, value => {\n      npmSearchText.value = value\n    })\n\n    const npmSearchResults = computed(() => {\n      if (npmSearchResult.value) {\n        const hits = npmSearchResult.value.hits\n        if (hits.length) {\n          const searchHits = result.value ? result.value.hits : []\n          const dictionary = searchHits.reduce((dic, hit) => {\n            dic[hit.name] = true\n            return dic\n          }, {})\n          return hits.filter(hit => !dictionary[hit.name])\n        }\n      }\n    })\n\n    function selectNpmResult (result) {\n      router.push({\n        name: 'add-package',\n        query: {\n          packageName: result.name,\n          projectTypeId: projectTypeId.value,\n        },\n      })\n      close()\n    }\n\n    return {\n      close,\n      input,\n      focusInput,\n      searchText,\n      result,\n      getRoute,\n      list,\n      focusIndex,\n      focusPrevious,\n      focusNext,\n      selectFocused,\n      projectTypeId,\n      projectTypeSlug,\n      hasFilters,\n      clearFilters,\n      npmSearchResults,\n      selectNpmResult,\n    }\n  },\n}\n</script>\n\n<template>\n  <FocusTrap active>\n    <div\n      class=\"overlay fixed z-30 inset-0 flex justify-center bg-blur\"\n      @keyup.esc=\"close()\"\n    >\n      <div\n        class=\"absolute inset-0 bg-gray-900 opacity-90\"\n        @click=\"close()\"\n      />\n\n      <div\n        class=\"relative m-2 sm:m-4 flex-auto max-w-3xl max-h-screen box flex flex-col items-center\"\n      >\n        <div class=\"lg:fixed lg:top-0 lg:right-0 lg:p-4\">\n          <BaseButton\n            icon-left=\"close\"\n            class=\"px-4 py-2 text-gray-600 hover:text-gray-500 hover:bg-gray-800\"\n            @click=\"close()\"\n          >\n            Close\n          </BaseButton>\n        </div>\n\n        <div class=\"flex items-center w-full mt-2 sm:mt-4 lg:mt-0 mb-4 bg-black rounded\">\n          <input\n            ref=\"input\"\n            :value=\"searchText\"\n            placeholder=\"Search...\"\n            maxlength=\"80\"\n            class=\"bg-transparent w-0 px-4 lg:px-8 py-4 flex-1\"\n            @input=\"searchText = $event.currentTarget.value\"\n            @keydown.up=\"focusPrevious()\"\n            @keydown.down=\"focusNext()\"\n            @keyup.enter=\"selectFocused()\"\n          >\n\n          <a\n            href=\"https://algolia.com/\"\n            target=\"_blank\"\n            class=\"mx-2 flex items-center opacity-50 hover:opacity-100\"\n          >\n            <span class=\"text-xs mr-1\">\n              Search by\n            </span>\n            <img\n              src=\"~@/assets/algolia.svg\"\n              alt=\"Search by Algolia\"\n              class=\"w-16 mt-1\"\n            >\n          </a>\n\n          <ProjectTypeSelect\n            v-model=\"projectTypeId\"\n            :project-type-slug.sync=\"projectTypeSlug\"\n            placeholder=\"All types\"\n            class=\"flex-none mx-2\"\n            button-class=\"bg-gray-800 hover:bg-gray-700 px-4 py-2\"\n          />\n\n          <BaseButton\n            v-tooltip=\"'Clear search'\"\n            :disabled=\"!hasFilters\"\n            icon-left=\"backspace\"\n            class=\"bg-gray-800 hover:bg-gray-700 w-10 h-10 mr-2\"\n            @click=\"clearFilters()\"\n          />\n        </div>\n\n        <LoadingIndicator\n          v-if=\"!result\"\n          class=\"p-8\"\n        />\n\n        <div\n          v-else-if=\"result.hits.length || npmSearchResults\"\n          ref=\"list\"\n          class=\"w-full flex-1 overflow-auto\"\n        >\n          <PackageListItem\n            v-for=\"(hit, index) of result.hits\"\n            :key=\"hit.id\"\n            :pkg=\"hit\"\n            :to=\"getRoute(hit)\"\n            class=\"mt-4 first:mt-0 border-l-2 border-transparent\"\n            :class=\"{\n              'focused': focusIndex === index,\n            }\"\n            @mouseover.native=\"focusIndex = index\"\n          />\n\n          <div\n            v-if=\"npmSearchResults\"\n            class=\"flex flex-col mt-8\"\n          >\n            <div class=\"p-2 sm:px-6\">\n              Other results\n            </div>\n\n            <BaseButton\n              v-for=\"result of npmSearchResults\"\n              :key=\"result.id\"\n              class=\"mt-4 p-2 sm:px-6 sm:py-4 bg-gray-800 hover:bg-gray-700\"\n              @click=\"selectNpmResult(result)\"\n            >\n              <div class=\"w-full text-left flex\">\n                <div class=\"flex-none mr-4\">\n                  {{ result.name }}\n                </div>\n                <div class=\"text-gray-600 truncate\">\n                  {{ result.description }}\n                </div>\n              </div>\n            </BaseButton>\n          </div>\n        </div>\n\n        <EmptyMessage\n          v-else\n        >\n          No results\n        </EmptyMessage>\n      </div>\n    </div>\n  </FocusTrap>\n</template>\n\n<style lang=\"postcss\" scoped>\n.focused {\n  @apply bg-gray-700;\n\n  &.router-link-active {\n    @apply bg-purple-800;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/search/SearchOverlayAsyncState.vue",
    "content": "<script>\nimport ErrorMessage from '../ErrorMessage.vue'\nimport LoadingIndicator from '../LoadingIndicator.vue'\n\nexport default {\n  components: {\n    ErrorMessage,\n    LoadingIndicator,\n  },\n\n  props: {\n    state: {\n      type: String,\n      required: true,\n    },\n  },\n}\n</script>\n\n<template>\n  <div\n    class=\"overlay fixed z-30 inset-0 flex justify-center bg-blur\"\n  >\n    <div\n      class=\"absolute inset-0 bg-gray-900 opacity-90\"\n    />\n\n    <div\n      class=\"relative m-2 sm:m-8 flex-auto max-w-3xl max-h-screen box flex items-center justify-center\"\n    >\n      <ErrorMessage\n        v-if=\"state === 'error'\"\n        error=\"Can't load Search Overlay component\"\n        @click.native=\"$emit('close')\"\n      />\n      <LoadingIndicator v-else-if=\"state === 'loading'\" />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/user/LoginView.vue",
    "content": "<script>\nimport { ref, watch } from '@vue/composition-api'\nimport { useCurrentUser } from './useCurrentUser'\nimport { STORE_ROUTE_BEFORE_REDIRECT, useRouter } from '@/util/router'\n\nexport default {\n  setup (props) {\n    // Redirect if already logged in\n    const { currentUser } = useCurrentUser()\n    const router = useRouter()\n    watch(currentUser, value => {\n      if (value) {\n        router.replace({ name: 'home' })\n      }\n    })\n\n    const loading = ref(null)\n\n    return {\n      loading,\n      baseUrl: process.env.VUE_APP_API_BASE,\n    }\n  },\n\n  beforeRouteEnter (from, to, next) {\n    if (to.name !== 'login') {\n      localStorage.setItem(STORE_ROUTE_BEFORE_REDIRECT, to.fullPath)\n    } else {\n      localStorage.removeItem(STORE_ROUTE_BEFORE_REDIRECT)\n    }\n    next()\n  },\n\n  metaInfo: {\n    title: 'Login',\n  },\n}\n</script>\n\n<template>\n  <div class=\"flex flex-col items-center\">\n    <h1 class=\"text-3xl text-gray-500 mt-20 mb-4\">\n      <i class=\"material-icons text-4xl text-purple-700 mr-2\">thumb_up</i>\n      Join our community now!\n    </h1>\n    <div class=\"mb-8\">\n      <router-link\n        :to=\"{ name: 'about-privacy' }\"\n        class=\"text-purple-400 hover:text-purple-300\"\n      >\n        Privacy &amp; Cookies\n      </router-link>\n    </div>\n    <div class=\"mt-4\">\n      <BaseButton\n        :href=\"`${baseUrl}/auth/github`\"\n        :loading=\"loading === 'github'\"\n        :disabled=\"!!loading\"\n        class=\"bg-gray-800 hover:bg-gray-700 w-64 py-4\"\n        @click=\"loading = 'github'\"\n      >\n        <img\n          src=\"~@/assets/github.png\"\n          alt=\"Github logo\"\n          class=\"w-6 h-6 mr-4\"\n        >\n        Sign in with GitHub\n      </BaseButton>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/user/NoBookmarkPackageSelected.vue",
    "content": "<template>\n  <div class=\"text-center my-8\">\n    <div\n      class=\"flex items-center justify-center mb-4\"\n    >\n      <i class=\"material-icons text-6xl text-gray-800\">bookmark_border</i>\n    </div>\n\n    <h1 class=\"text-xl font-light text-gray-600\">\n      <i class=\"material-icons mr-2 text-gray-700\">arrow_back</i>\n      <span>Select a package to continue</span>\n    </h1>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/user/UserCheckSignedIn.vue",
    "content": "<script>\nimport { watch } from '@vue/composition-api'\nimport { useCurrentUser } from './useCurrentUser'\nimport { useRouter } from '@/util/router'\n\nexport default {\n  setup (props) {\n    const { currentUser, loading } = useCurrentUser()\n    const router = useRouter()\n    watch(currentUser, value => {\n      if (!loading.value && !value) {\n        router.replace({ name: 'login' })\n      }\n    })\n  },\n\n  render (h) {\n    return null\n  },\n}\n</script>\n"
  },
  {
    "path": "packages/frontend/src/components/user/UserDashboard.vue",
    "content": "<script>\nimport LoadingIndicator from '../LoadingIndicator.vue'\nimport UserCheckSignedIn from './UserCheckSignedIn.vue'\nimport PageTitle from '../PageTitle.vue'\nimport RouteTab from '../RouteTab'\n\nimport { useCurrentUser } from './useCurrentUser'\n\nexport default {\n  components: {\n    LoadingIndicator,\n    UserCheckSignedIn,\n    PageTitle,\n    RouteTab,\n  },\n\n  setup () {\n    const { currentUser, loading } = useCurrentUser()\n\n    return {\n      currentUser,\n      loading,\n    }\n  },\n\n  metaInfo: {\n    title: 'My Dashboard',\n  },\n}\n</script>\n\n<template>\n  <div>\n    <UserCheckSignedIn />\n\n    <LoadingIndicator\n      v-if=\"loading\"\n      class=\"py-8\"\n    />\n\n    <div v-else-if=\"currentUser\">\n      <PageTitle class=\"justify-center mb-4 lg:pl-4\">\n        Hello {{ currentUser.nickname }}! 👋️\n      </PageTitle>\n\n      <div class=\"overflow-x-auto flex pb-4 lg:pb-0\">\n        <RouteTab\n          :to=\"{ name: 'user-dashboard' }\"\n          exact\n        >\n          Dashboard\n        </RouteTab>\n\n        <RouteTab\n          :to=\"{ name: 'user-bookmarks' }\"\n        >\n          Bookmarks\n        </RouteTab>\n      </div>\n\n      <router-view :user=\"currentUser\" />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/user/UserMenu.vue",
    "content": "<script>\nimport { useCurrentUser } from './useCurrentUser'\n\nexport default {\n  setup () {\n    const { currentUser, loading, isAdmin } = useCurrentUser()\n\n    return {\n      currentUser,\n      loading,\n      isAdmin,\n      baseUrl: process.env.VUE_APP_API_BASE,\n    }\n  },\n}\n</script>\n\n<template>\n  <div\n    v-if=\"loading\"\n    class=\"w-10 h-10 rounded-full bg-gray-700 border-2 border-gray-700\"\n  />\n  <div v-else>\n    <div\n      v-if=\"currentUser\"\n      class=\"w-10 h-10 flex items-center justify-center\"\n    >\n      <BasePopper\n        theme=\"yellow-arrow\"\n        placement=\"bottom-end\"\n      >\n        <BaseButton>\n          <img\n            :src=\"currentUser.avatar || require('@/assets/user.svg')\"\n            alt=\"User\"\n            class=\"avatar max-w-full max-h-full rounded-full bg-gray-700 border-2 border-gray-700 hover:border-gray-600\"\n            :class=\"{\n              'border-red-700 hover:border-red-600': currentUser.admin,\n            }\"\n          >\n        </BaseButton>\n\n        <template #popper>\n          <div class=\"flex flex-col items-stretch pb-4\">\n            <div class=\"px-6 py-3 mb-4 text-yellow-500 bg-yellow-900 rounded-t\">\n              <i class=\"material-icons text-lg mr-1\">account_circle</i>\n              {{ currentUser.nickname }}\n            </div>\n\n            <BaseButton\n              v-if=\"isAdmin\"\n              v-close-popper\n              :to=\"{ name: 'admin' }\"\n              icon-left=\"lock\"\n              align=\"left\"\n              class=\"px-6 py-2 text-red-300 hover:bg-red-800\"\n              square\n            >\n              Admin\n            </BaseButton>\n\n            <BaseButton\n              v-close-popper\n              :to=\"{ name: 'user-dashboard' }\"\n              icon-left=\"dashboard\"\n              align=\"left\"\n              class=\"px-6 py-2 hover:bg-gray-800\"\n              square\n            >\n              Dashboard\n            </BaseButton>\n\n            <BaseButton\n              v-close-popper\n              :to=\"{ name: 'user-bookmarks' }\"\n              icon-left=\"bookmark\"\n              align=\"left\"\n              class=\"px-6 py-2 hover:bg-gray-800\"\n              square\n            >\n              Bookmarks\n            </BaseButton>\n\n            <BaseButton\n              :href=\"`${baseUrl}/auth/logout`\"\n              icon-left=\"power_settings_new\"\n              align=\"left\"\n              class=\"px-6 py-2 hover:bg-gray-800\"\n              square\n            >\n              Logout\n            </BaseButton>\n          </div>\n        </template>\n      </BasePopper>\n    </div>\n    <div v-else>\n      <BaseButton\n        :to=\"{ name: 'login' }\"\n        icon-left=\"face\"\n        class=\"px-4 py-2 bg-gray-800 text-gray-200 hover:bg-gray-700\"\n      >\n        Sign in\n      </BaseButton>\n    </div>\n  </div>\n</template>\n\n<style lang=\"postcss\" scoped>\n.v-popper--open {\n  .avatar {\n    @apply border-yellow-800;\n  }\n}\n</style>\n"
  },
  {
    "path": "packages/frontend/src/components/user/UserTabBookmarks.vue",
    "content": "<script>\nimport LoadingIndicator from '../LoadingIndicator.vue'\nimport PackageListItem from '../pkg/PackageListItem.vue'\n\nimport gql from 'graphql-tag'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { pkgFragment } from '../pkg/fragments'\n\nexport default {\n  components: {\n    LoadingIndicator,\n    PackageListItem,\n  },\n\n  props: {\n    user: {\n      type: Object,\n      required: true,\n    },\n\n    packageId: {\n      type: String,\n      default: null,\n    },\n  },\n\n  setup () {\n    const { result, loading } = useQuery(gql`\n      query UserPackageBookmarks {\n        currentUser {\n          id\n          bookmarkedPackages {\n            ...pkg\n          }\n        }\n      }\n      ${pkgFragment}\n    `)\n    const packages = useResult(result, [], data => data.currentUser.bookmarkedPackages)\n\n    return {\n      packages,\n      loading,\n    }\n  },\n\n  metaInfo: {\n    title: 'My Bookmarks',\n  },\n}\n</script>\n\n<template>\n  <div>\n    <div class=\"flex\">\n      <div\n        v-if=\"!$responsive.lg || !packageId\"\n        class=\"w-full lg:w-1/3 lg:pb-64 lg:mt-4 lg:sticky lg:top-4 lg:max-h-screen lg:overflow-y-auto\"\n      >\n        <LoadingIndicator\n          v-if=\"loading\"\n          class=\"py-8\"\n        />\n        <template v-else>\n          <PackageListItem\n            v-for=\"pkg of packages\"\n            :key=\"pkg.id\"\n            :to=\"{\n              name: $route.path.includes('/pkg/') && $route.params.packageId !== pkg.id ? undefined : 'user-bookmarks-package',\n              params: { packageId: pkg.id },\n            }\"\n            :pkg=\"pkg\"\n            class=\"mb-6\"\n          />\n        </template>\n      </div>\n      <div\n        v-if=\"!$responsive.lg || packageId\"\n        class=\"w-full lg:w-2/3 lg:pl-16\"\n      >\n        <router-view />\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/user/UserTabHome.vue",
    "content": "<script>\nimport UserTeams from './UserTeams.vue'\n\nexport default {\n  components: {\n    UserTeams,\n  },\n}\n</script>\n\n<template>\n  <div>\n    <UserTeams />\n\n    <div class=\"mt-16 flex flex-col items-center text-gray-600\">\n      <i class=\"material-icons text-5xl\">build</i>\n\n      <span class=\"text-xl mt-4\">Work in progress</span>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/user/UserTeams.vue",
    "content": "<script>\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\n\nexport default {\n  setup (props) {\n    const { result } = useQuery(gql`\n      query UserTeams {\n        currentUser {\n          id\n          teams {\n            id\n            name\n          }\n        }\n      }\n    `)\n    const teams = useResult(result, [], data => data.currentUser.teams)\n\n    return {\n      teams,\n    }\n  },\n}\n</script>\n\n<template>\n  <div\n    v-if=\"teams.length\"\n    class=\"my-4 flex items-center p-4 border-orange-900 border-2 rounded\"\n  >\n    <div>Your teams:</div>\n    <div\n      v-for=\"team of teams\"\n      :key=\"team.id\"\n      class=\"ml-4 rounded text-orange-300 bg-orange-700 flex items-center px-4 py-2\"\n    >\n      <i class=\"material-icons mr-2\">supervisor_account</i>\n      {{ team.name }}\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/user/fragments.js",
    "content": "import gql from 'graphql-tag'\n\nexport const userFragment = gql`\nfragment user on User {\n  id\n  email\n  nickname\n  avatar\n  admin\n}\n`\n"
  },
  {
    "path": "packages/frontend/src/components/user/useCurrentUser.js",
    "content": "import gql from 'graphql-tag'\nimport { useQuery, useResult } from '@vue/apollo-composable'\nimport { userFragment } from './fragments'\nimport { computed } from '@vue/composition-api'\n\nexport function useCurrentUser () {\n  const { result, loading } = useQuery(gql`\n    query CurrentUser {\n      currentUser {\n        ...user\n      }\n    }\n    ${userFragment}\n  `)\n\n  const currentUser = useResult(result)\n\n  const isAdmin = computed(() => !!(currentUser.value && currentUser.value.admin))\n\n  return {\n    currentUser,\n    isAdmin,\n    loading,\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/components.js",
    "content": "// --- Base components ---\nimport Vue from 'vue'\n\n// Require all the components that start with 'BaseXXX.vue'\nconst requireComponents = require.context('./components', true, /Base[a-z0-9]+\\.(jsx?|vue)$/i)\nrequireComponents.keys().forEach(fileName => {\n  const component = requireComponents(fileName)\n  const name = fileName.match(/([a-z0-9]+)\\./i)[1]\n  Vue.component(name, component.default || component)\n})\n"
  },
  {
    "path": "packages/frontend/src/index.d.ts",
    "content": "import 'vue-apollo'\n"
  },
  {
    "path": "packages/frontend/src/main.js",
    "content": "import 'focus-visible'\nimport './plugins'\nimport Vue from 'vue'\nimport App from './components/App.vue'\nimport router from './router'\nimport './registerServiceWorker'\nimport { createClient } from './vue-apollo'\nimport './components'\nimport { provide } from '@vue/composition-api'\nimport { DefaultApolloClient } from '@vue/apollo-composable'\n\nVue.config.productionTip = false\nVue.config.devtools = true\n\nVue.config.errorHandler = (err, vm, info) => {\n  console.error(err, vm, info)\n}\n\nconst apolloClient = createClient()\n\nconst app = new Vue({\n  router,\n\n  setup () {\n    provide(DefaultApolloClient, apolloClient)\n  },\n\n  render: h => h(App),\n})\n\nrouter.onReady(() => {\n  app.$mount('#app')\n})\n"
  },
  {
    "path": "packages/frontend/src/plugins.js",
    "content": "import Vue from 'vue'\nimport Responsive from './util/responsive'\nimport { focus } from 'vue-focus'\nimport VueCompositionApi from '@vue/composition-api'\nimport VueMeta from 'vue-meta'\nimport VTooltip from 'v-tooltip'\nimport 'v-tooltip/dist/v-tooltip.css'\nimport GlobalEvents from 'vue-global-events'\n\nVue.use(Responsive, {\n  computed: {\n    sm () {\n      return this.width < 640\n    },\n    md () {\n      return this.width < 768\n    },\n    mobile () {\n      return this.width <= 768\n    },\n    lg () {\n      return this.width < 1024\n    },\n    xl () {\n      return this.width < 1280\n    },\n  },\n})\n\nVue.directive('focus', focus)\n\nVue.use(VueCompositionApi)\n\nVue.use(VueMeta)\n\nVue.use(VTooltip, {\n  boundariesElement: 'viewport',\n  themes: {\n    tooltip: {\n      // Delay (ms)\n      delay: {\n        show: 700,\n        hide: 0,\n      },\n      offset: 8,\n      instantMove: true,\n    },\n    dropdown: {\n      offset: 8,\n    },\n    'yellow-arrow': {\n      $extend: 'dropdown',\n    },\n  },\n})\n\nVue.component('GlobalEvents', GlobalEvents)\n"
  },
  {
    "path": "packages/frontend/src/registerServiceWorker.js",
    "content": "/* eslint-disable no-console */\n\nimport { register } from 'register-service-worker'\nimport { updateAvailable, updateRegistration } from './util/service-worker'\n\nif (process.env.NODE_ENV === 'production') {\n  register(`${process.env.BASE_URL}service-worker.js`, {\n    ready () {\n      console.log(\n        'App is being served from cache by a service worker.\\n' +\n        'For more details, visit https://goo.gl/AFskqB'\n      )\n    },\n    registered () {\n      console.log('Service worker has been registered.')\n    },\n    cached () {\n      console.log('Content has been cached for offline use.')\n    },\n    updatefound () {\n      console.log('New content is downloading.')\n    },\n    updated (registration) {\n      console.log('New content is available; please refresh.')\n      updateRegistration.value = registration\n      updateAvailable.value = true\n    },\n    offline () {\n      console.log('No internet connection found. App is running in offline mode.')\n    },\n    error (error) {\n      console.error('Error during service worker registration:', error)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/frontend/src/router.js",
    "content": "import Vue from 'vue'\nimport Router from 'vue-router'\nimport AppHome from './components/app/AppHome.vue'\nimport { scrollBehavior } from './util/scroll-behavior'\n\nVue.use(Router)\n\nconst packageRoute = (namePrefix = '') => ({\n  path: 'pkg/:packageId',\n  component: () => import(/* webpackChunkName: \"package-view\" */ './components/pkg/PackageView.vue'),\n  props: route => ({\n    ...route.params,\n    routePrefix: namePrefix,\n  }),\n  meta: {\n    keepScroll: true,\n  },\n  children: [\n    {\n      path: '',\n      name: `${namePrefix}package`,\n      component: () => import(/* webpackChunkName: \"package-tab-general\" */ './components/pkg/PackageTabGeneral.vue'),\n    },\n    {\n      path: 'edit',\n      name: `${namePrefix}package-edit`,\n      component: () => import(/* webpackChunkName: \"package-tab-edit\" */ './components/pkg/PackageTabEdit.vue'),\n    },\n    {\n      path: 'data-sources',\n      name: `${namePrefix}package-data-sources`,\n      component: () => import(/* webpackChunkName: \"package-tab-data-sources\" */ './components/pkg/PackageTabDataSources.vue'),\n    },\n    {\n      path: 'releases',\n      name: `${namePrefix}package-releases`,\n      component: () => import(/* webpackChunkName: \"package-tab-releases\" */ './components/pkg/PackageTabReleases.vue'),\n    },\n    {\n      path: 'insight',\n      component: () => import(/* webpackChunkName: \"package-tab-insight\" */ './components/pkg/PackageTabInsight.vue'),\n      meta: {\n        depthWeight: 0,\n      },\n      children: [\n        {\n          path: '',\n          name: `${namePrefix}package-insight`,\n          component: () => import(/* webpackChunkName: \"package-tab-insight-npm-downloads\" */ './components/pkg/PackageInsightNpmDownloads.vue'),\n        },\n      ],\n    },\n  ],\n})\n\nexport default new Router({\n  mode: 'history',\n  base: process.env.BASE_URL,\n  scrollBehavior,\n  routes: [\n    {\n      path: '/',\n      name: 'home',\n      component: AppHome,\n    },\n    {\n      path: '/login',\n      name: 'login',\n      component: () => import(/* webpackChunkName: \"login\" */ './components/user/LoginView.vue'),\n    },\n    {\n      path: '/me',\n      component: () => import(/* webpackChunkName: \"user-dashboard\" */ './components/user/UserDashboard.vue'),\n      children: [\n        {\n          path: '',\n          name: 'user-dashboard',\n          component: () => import(/* webpackChunkName: \"user-tab-home\" */ './components/user/UserTabHome.vue'),\n          meta: {\n            depthWeight: 2,\n          },\n        },\n        {\n          path: 'bookmarks',\n          component: () => import(/* webpackChunkName: \"user-tab-bookmarks\" */ './components/user/UserTabBookmarks.vue'),\n          props: true,\n          children: [\n            {\n              path: '',\n              name: 'user-bookmarks',\n              component: () => import(/* webpackChunkName: \"no-package-bookmar-selected\" */ './components/user/NoBookmarkPackageSelected.vue'),\n            },\n            packageRoute('user-bookmarks-'),\n          ],\n        },\n      ],\n    },\n    {\n      path: '/for/:projectTypeSlug',\n      component: () => import(/* webpackChunkName: \"project-type\" */ './components/project-type/ProjectTypeView.vue'),\n      props: true,\n      children: [\n        {\n          path: '',\n          name: 'project-type',\n          component: () => import(/* webpackChunkName: \"no-package-selected\" */ './components/pkg/NoPackageSelected.vue'),\n        },\n        packageRoute(),\n      ],\n    },\n    {\n      path: '/proposed/:projectTypeSlug',\n      component: () => import(/* webpackChunkName: \"project-type-package-proposals\" */ './components/project-type/ProjectTypePackageProposalsView.vue'),\n      props: true,\n      meta: {\n        depthWeight: 2,\n      },\n      children: [\n        {\n          path: '',\n          name: 'project-type-proposals',\n          component: () => import(/* webpackChunkName: \"no-package-selected\" */ './components/pkg/NoPackageSelected.vue'),\n        },\n        {\n          path: 'pkg/:packageId',\n          component: () => import(/* webpackChunkName: \"package-proposal-view\" */ './components/pkg/PackageProposalView.vue'),\n          props: true,\n          meta: {\n            keepScroll: true,\n          },\n          children: [\n            {\n              path: '',\n              name: `package-proposal`,\n              component: () => import(/* webpackChunkName: \"package-proposal-tab-general\" */ './components/pkg/PackageProposalTabGeneral.vue'),\n            },\n            {\n              path: 'edit',\n              name: `package-proposal-edit`,\n              component: () => import(/* webpackChunkName: \"package-proposal-tab-edit\" */ './components/pkg/PackageProposalTabEdit.vue'),\n            },\n            {\n              path: 'data-sources',\n              name: `package-proposal-data-sources`,\n              component: () => import(/* webpackChunkName: \"package-tab-data-sources\" */ './components/pkg/PackageTabDataSources.vue'),\n            },\n            {\n              path: 'releases',\n              name: `package-proposal-releases`,\n              component: () => import(/* webpackChunkName: \"package-tab-releases\" */ './components/pkg/PackageTabReleases.vue'),\n            },\n            {\n              path: 'insight',\n              component: () => import(/* webpackChunkName: \"package-tab-insight\" */ './components/pkg/PackageTabInsight.vue'),\n              meta: {\n                depthWeight: 0,\n              },\n              children: [\n                {\n                  path: '',\n                  name: `package-proposal-insight`,\n                  component: () => import(/* webpackChunkName: \"package-tab-insight-npm-downloads\" */ './components/pkg/PackageInsightNpmDownloads.vue'),\n                },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n    {\n      path: '/pkg/add',\n      name: 'add-package',\n      component: () => import(/* webpackChunkName: \"package-add-wizard\" */ './components/pkg/PackageAddWizard.vue'),\n    },\n    {\n      path: '/about/privacy',\n      name: 'about-privacy',\n      component: () => import(/* webpackChunkName: \"about-privacy\" */ './components/about/Privacy.vue'),\n    },\n    {\n      path: '/about/contributing',\n      name: 'about-contributing',\n      component: () => import(/* webpackChunkName: \"about-contributing\" */ './components/about/Contributing.vue'),\n    },\n    {\n      path: '/admin',\n      component: () => import(/* webpackChunkName: \"admin-dashboard\" */ './components/admin/AdminDashboard.vue'),\n      children: [\n        {\n          path: '',\n          name: 'admin',\n          component: () => import(/* webpackChunkName: \"admin-home\" */ './components/admin/AdminHome.vue'),\n        },\n        {\n          path: 'teams',\n          component: () => import(/* webpackChunkName: \"admin-teams\" */ './components/admin/AdminTeams.vue'),\n          props: true,\n          children: [\n            {\n              path: '',\n              name: 'admin-teams',\n              component: { render () { return null } },\n            },\n            {\n              path: 'create',\n              name: 'admin-team-create',\n              component: () => import(/* webpackChunkName: \"admin-team-create\" */ './components/admin/AdminTeamCreate.vue'),\n            },\n            {\n              path: ':teamId',\n              name: 'admin-team-view',\n              props: true,\n              component: () => import(/* webpackChunkName: \"admin-team-view\" */ './components/admin/AdminTeamView.vue'),\n            },\n          ],\n        },\n      ],\n    },\n  ],\n})\n"
  },
  {
    "path": "packages/frontend/src/util/algolia-npm.js",
    "content": "import * as Algolia from 'algoliasearch'\nimport { useSearch } from './algolia'\n\nconst client = Algolia('OFCNCOG2CU', 'db283631f89b5b8a10707311f911fd00')\n\nexport function useNpmSearch (queryParameters = {}) {\n  return useSearch('npm-search', queryParameters, {\n    attributesToRetrieve: [\n      'name',\n      'description',\n      'repository',\n      'keywords',\n    ],\n    hitsPerPage: 10,\n  }, client, {\n    skipEmptyQuery: true,\n  })\n}\n"
  },
  {
    "path": "packages/frontend/src/util/algolia.js",
    "content": "import * as Algolia from 'algoliasearch'\nimport { ref, isRef, watch } from '@vue/composition-api'\n\nconst defaultClient = Algolia(process.env.VUE_APP_ALGOLIA_ID, process.env.VUE_APP_ALGOLIA_KEY)\n\n/** @typedef {import('@vue/composition-api').Ref} Ref */\n/** @typedef {import('algoliasearch').QueryParameters} QueryParameters */\n/** @typedef {import('algoliasearch').Response} Response */\n\n/**\n * @param {string} indexName\n * @param {QueryParameters | Ref<QueryParameters>} queryParameters\n * @param {QueryParameters} defaultQueryParameters\n * @param {Algolia.Client} algoliaClient\n * @param {any} options Other options\n */\nexport function useSearch (indexName, queryParameters = {}, defaultQueryParameters = {}, algoliaClient = defaultClient, options = {}) {\n  const index = algoliaClient.initIndex(indexName)\n  const searchText = ref('')\n  /** @type {Ref<Response>} */\n  const result = ref(null)\n\n  watch(searchText, () => search())\n  watch(isRef(queryParameters) ? queryParameters : () => queryParameters, () => search(), {\n    lazy: true,\n    deep: true,\n  })\n\n  async function search () {\n    if (options.skipEmptyQuery && !searchText.value) {\n      result.value = null\n    } else {\n      const response = await index.search({\n        query: searchText.value,\n        ...defaultQueryParameters,\n        ...isRef(queryParameters) ? queryParameters.value : queryParameters,\n      })\n      response.hits.forEach((hit) => {\n        hit.id = hit.objectID\n      })\n      result.value = response\n    }\n  }\n\n  return {\n    searchText,\n    result,\n    search,\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/util/emoji.js",
    "content": "import { ref, computed, watch } from '@vue/composition-api'\n\nconst joypixels = () => import(\n  /* webpackChunkName: \"emoji-toolkit\" */\n  /* webpackMode: \"lazy\" */\n  'emoji-toolkit'\n)\n\nexport function useEmoji (sourceText) {\n  const sourceRef = typeof sourceText === 'function' ? computed(sourceText) : sourceText\n  const parsedText = ref(sourceRef.value)\n\n  watch(sourceRef, async value => {\n    const { default: emoji } = await joypixels()\n    parsedText.value = emoji.shortnameToUnicode(sourceRef.value)\n  })\n\n  return {\n    parsedText,\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/util/env.js",
    "content": "export const isMac = navigator.platform.startsWith('Mac')\n"
  },
  {
    "path": "packages/frontend/src/util/error.js",
    "content": "import router from '../router'\n\nexport function checkNeedLogin (e) {\n  if (e.graphQLErrors && e.graphQLErrors.some(e => e.extensions.code === 'guest')) {\n    router.push({ name: 'login' })\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/util/favicon.js",
    "content": "export function setFavicon (url) {\n  document.head.querySelectorAll('link[rel*=\"icon\"]').forEach(el => {\n    el.href = url\n  })\n}\n\nexport function resetFavicon () {\n  document.head.querySelectorAll('link[rel*=\"icon\"]').forEach(el => {\n    el.href = `${process.env.BASE_URL}favicon.png`\n  })\n}\n"
  },
  {
    "path": "packages/frontend/src/util/installation.js",
    "content": "import { ref } from '@vue/composition-api'\n\nexport const installationAvailable = ref(false)\n"
  },
  {
    "path": "packages/frontend/src/util/lock-scroll.js",
    "content": "import { ref, watch, onMounted, onActivated, onDeactivated, onUnmounted } from '@vue/composition-api'\n\nconst CLASS = 'no-scroll'\n\nexport function useLockScroll (selector = 'body, .auto-lock-scroll', auto = true) {\n  const locked = ref(false)\n\n  function update () {\n    const els = document.querySelectorAll(selector)\n    els.forEach(el => {\n      if (locked.value) {\n        el.classList.add(CLASS)\n      } else {\n        el.classList.remove(CLASS)\n      }\n    })\n  }\n\n  watch(locked, () => update())\n\n  function lock () {\n    locked.value = true\n  }\n\n  function unlock () {\n    locked.value = false\n  }\n\n  if (auto) {\n    onMounted(lock)\n    onActivated(lock)\n    onDeactivated(unlock)\n    onUnmounted(() => {\n      unlock()\n      // Need to manually update as the instance is being destroyed\n      // (no more reactivity updates)\n      update()\n    })\n  }\n\n  return {\n    locked,\n    lock,\n    unlock,\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/util/proposal.js",
    "content": "import { useRouter } from './router'\nimport gql from 'graphql-tag'\nimport { pkgProposalFragment } from '@/components/pkg/fragments'\nimport { useApolloClient } from '@vue/apollo-composable'\n\nexport function useSelectNextProposal () {\n  const apolloClient = useApolloClient()\n  const router = useRouter()\n\n  async function selectNext (projectTypeId, proposalId) {\n    const { data } = await apolloClient.client.query({\n      query: gql`\n        query ProjectTypePackages ($id: ID!) {\n          projectType (id: $id) {\n            id\n            packageProposals {\n              id\n            }\n          }\n        }\n      `,\n      variables: {\n        id: projectTypeId,\n      },\n    })\n    const proposals = data.projectType.packageProposals\n\n    if (proposals.length === 1) {\n      router.push({ name: 'project-type-proposals' })\n    } else {\n      let index = proposals.findIndex(p => p.id === proposalId)\n      if (index === proposals.length - 1) {\n        index = 0\n      } else {\n        index++\n      }\n      const nextProposal = proposals[index]\n      if (nextProposal) {\n        router.push({\n          name: 'package-proposal',\n          params: { packageId: nextProposal.id },\n        })\n      }\n    }\n  }\n\n  return {\n    selectNext,\n  }\n}\n\nexport function removeProposalFromCache (cache, projectTypeId, proposalId) {\n  const query = {\n    query: gql`\n      query ProjectTypePackages ($id: ID!) {\n        projectType (id: $id) {\n          id\n          packageProposals {\n            ...pkgProposal\n          }\n        }\n      }\n      ${pkgProposalFragment}\n    `,\n    variables: {\n      id: projectTypeId,\n    },\n  }\n  const data = cache.readQuery(query)\n  const list = data.projectType.packageProposals\n  const index = list.findIndex(p => p.id === proposalId)\n  if (index !== -1) {\n    list.splice(index, 1)\n    cache.writeQuery({\n      ...query,\n      data,\n    })\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/util/qrcode.js",
    "content": "import QRCode from 'qrcode'\nimport { ref, watch, isRef } from '@vue/composition-api'\n\nexport function useQrcode (url) {\n  const src = ref('')\n\n  watch(isRef(url) ? url : () => url, async (value) => {\n    if (value) {\n      src.value = await QRCode.toDataURL(value, {\n        errorCorrectionLevel: 'H',\n      })\n    }\n  })\n\n  return src\n}\n"
  },
  {
    "path": "packages/frontend/src/util/responsive.js",
    "content": "import { computed } from '@vue/composition-api'\n\nexport let responsive\n\nlet computedFields\n\nexport default {\n  install (Vue, options) {\n    const finalOptions = Object.assign({}, {\n      computed: {},\n    }, options)\n\n    responsive = new Vue({\n      data () {\n        return {\n          width: window.innerWidth,\n          height: window.innerHeight,\n        }\n      },\n      computed: finalOptions.computed,\n    })\n\n    computedFields = Object.keys(finalOptions.computed)\n\n    Object.defineProperty(Vue.prototype, '$responsive', {\n      get: () => responsive,\n    })\n\n    window.addEventListener('resize', () => {\n      responsive.width = window.innerWidth\n      responsive.height = window.innerHeight\n    })\n  },\n}\n\nexport function useResponsive () {\n  return {\n    width: computed(() => responsive.width),\n    height: computed(() => responsive.height),\n    ...computedFields.reduce((obj, key) => {\n      obj[key] = computed(() => responsive[key])\n      return obj\n    }, {}),\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/util/router.js",
    "content": "import { getCurrentInstance, computed } from '@vue/composition-api'\n\nexport const STORE_ROUTE_BEFORE_REDIRECT = 'dev.awesomejs.route.before-redirect'\n\nexport function useRouter () {\n  return getCurrentInstance().$router\n}\n\nexport function useRoute () {\n  const vm = getCurrentInstance()\n  return computed(() => vm.$route)\n}\n\nexport function getNamedParents (routes, matched) {\n  let parent = null\n  const parents = []\n  const parentPath = []\n  for (const record of matched) {\n    let path = record.path\n    if (!path) {\n      path = '/'\n    }\n    if (parent) {\n      parentPath.push(parent.path)\n    }\n    if (parentPath.length) {\n      path = path.substr(parentPath.join('/').length + 1)\n    }\n    let next = routes.find(r => r.path === path)\n    if (next.path) {\n      parent = next\n      routes = next.children\n      if (!next.name && next.children) {\n        next = next.children.find(r => !r.path)\n      }\n      parents.push(next)\n    } else {\n      break\n    }\n  }\n  return parents.slice(0, parents.length - 1)\n}\n\nexport function computeDepthWeight (route) {\n  return route.matched.reduce((total, m) => {\n    if (typeof m.meta.depthWeight === 'number') {\n      total += m.meta.depthWeight\n    } else {\n      total++\n    }\n    return total\n  }, 0)\n}\n"
  },
  {
    "path": "packages/frontend/src/util/scroll-behavior.js",
    "content": "// scrollBehavior:\n// - only available in html5 history mode\n// - defaults to no scroll behavior\n// - return false to prevent scroll\nexport const scrollBehavior = function (to, from, savedPosition) {\n  if (savedPosition) {\n    // savedPosition is only available for popstate navigations.\n    return savedPosition\n  } else {\n    const position = {}\n\n    // scroll to anchor by returning the selector\n    if (to.hash) {\n      position.selector = to.hash\n\n      // bypass #1number check\n      if (/^#\\d/.test(to.hash) || document.querySelector(to.hash)) {\n        return position\n      }\n\n      // if the returned position is falsy or an empty object,\n      // will retain current scroll position.\n      return false\n    }\n\n    if (window.innerWidth < 1024 || !to.matched.some(m => m.meta.keepScroll)) {\n      // coords will be used if no selector is provided,\n      // or if the selector didn't match any element.\n      position.x = 0\n      position.y = 0\n    }\n\n    return position\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/util/scroll.js",
    "content": "import { onUnmounted, watch } from '@vue/composition-api'\n\nfunction getScrollParent (node) {\n  if (node == null) {\n    return null\n  }\n\n  if (node.scrollHeight > node.clientHeight || (node.classList && node.classList.contains('scroll-parent'))) {\n    if (node === document.documentElement) {\n      return { emitter: document, scroller: document.documentElement }\n    }\n    return { emitter: node, scroller: node }\n  } else {\n    return getScrollParent(node.parentNode)\n  }\n}\n\nexport function onScrollBottom (handler, el, offsetFromBottom) {\n  let scrollParent\n\n  function onScroll () {\n    const { scroller } = scrollParent\n    if (scroller.scrollTop >= scroller.scrollHeight - scroller.clientHeight - offsetFromBottom) {\n      handler()\n    }\n  }\n\n  function addListeners () {\n    removeListeners()\n    if (!el.value) return\n    scrollParent = getScrollParent(el.value)\n    if (scrollParent) {\n      scrollParent.emitter.addEventListener('scroll', onScroll)\n    }\n  }\n\n  function removeListeners () {\n    if (scrollParent) {\n      scrollParent.emitter.removeEventListener('scroll', onScroll)\n    }\n  }\n\n  watch(el, () => {\n    addListeners()\n  })\n\n  onUnmounted(() => {\n    removeListeners()\n  })\n}\n"
  },
  {
    "path": "packages/frontend/src/util/service-worker.js",
    "content": "import { ref } from '@vue/composition-api'\n\nexport const updateAvailable = ref(false)\nexport const updateRegistration = ref(null)\n\nexport function applyUpdate () {\n  if (updateRegistration.value.installing) {\n    updateRegistration.value.installing.addEventListener('statechange', () => {\n      if (updateRegistration.value.installing.state === 'installed') {\n        refreshApp(updateRegistration.value.installing)\n      }\n    })\n  } else if (updateRegistration.value.waiting) {\n    refreshApp(updateRegistration.value.waiting)\n  }\n}\n\nexport function useAppUpdate () {\n  return {\n    updateAvailable,\n    applyUpdate,\n  }\n}\n\nfunction refreshApp (sw) {\n  let refreshing = false\n  navigator.serviceWorker.addEventListener('controllerchange', () => {\n    if (refreshing) return\n    refreshing = true\n    window.location.reload()\n  })\n  sw.postMessage({ type: 'SKIP_WAITING' })\n}\n"
  },
  {
    "path": "packages/frontend/src/util/share.js",
    "content": "export function useShare (callback = (info) => info) {\n  if (navigator.share) {\n    return (info) => navigator.share(callback(info))\n  }\n  return false\n}\n"
  },
  {
    "path": "packages/frontend/src/util/tags.js",
    "content": "import { computed } from '@vue/composition-api'\nimport { useQuery } from '@vue/apollo-composable'\nimport gql from 'graphql-tag'\n\nexport function useTags (pkg) {\n  if (typeof pkg === 'function') {\n    pkg = computed(pkg)\n  }\n  const tags = computed(() => pkg.value._tags || pkg.value.info.tags)\n  const isOfficial = computed(() => tags.value.includes('official'))\n\n  return {\n    tags,\n    isOfficial,\n  }\n}\n\nexport function useAvailableTags (projectTypeIdRef, formTags) {\n  if (typeof projectTypeIdRef === 'function') {\n    projectTypeIdRef = computed(projectTypeIdRef)\n  }\n  if (typeof formTags === 'function') {\n    formTags = computed(formTags)\n  }\n  const { result: projectTypeResult } = useQuery(gql`\n    query ProjectTypeTags ($id: ID!) {\n      projectType (id: $id) {\n        id\n        tags {\n          id\n        }\n      }\n    }\n  `, () => ({\n    id: projectTypeIdRef.value,\n  }), () => ({\n    enabled: !!projectTypeIdRef.value,\n  }))\n  const availableTags = computed(() => {\n    return Array.from(new Set([\n      ...formTags.value,\n      ...projectTypeResult.value ? projectTypeResult.value.projectType.tags.map(t => t.id) : [],\n    ]))\n  })\n\n  return {\n    availableTags,\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/vue-apollo.js",
    "content": "import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'\nimport { onError } from 'apollo-link-error'\nimport { logErrorMessages } from '@vue/apollo-util'\nimport { checkNeedLogin } from '@/util/error'\nimport { cache } from './cache'\n\n// Name of the localStorage item\nconst AUTH_TOKEN = 'apollo-token'\n\n// Http endpoint\nconst httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4040/graphql'\n\n// Config\nconst defaultOptions = {\n  // You can use `https` for secure connection (recommended in production)\n  httpEndpoint,\n  // You can use `wss` for secure connection (recommended in production)\n  // Use `null` to disable subscriptions\n  // wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4040/graphql',\n  wsEndpoint: null,\n  // LocalStorage token\n  tokenName: AUTH_TOKEN,\n  // Enable Automatic Query persisting with Apollo Engine\n  persisting: false,\n  // Use websockets for everything (no HTTP)\n  // You need to pass a `wsEndpoint` for this to work\n  websocketsOnly: false,\n  // Is being rendered on the server?\n  ssr: false,\n\n  httpLinkOptions: {\n    credentials: 'include',\n  },\n\n  // Override default apollo link\n  // note: don't override httpLink here, specify httpLink options in the\n  // httpLinkOptions property of defaultOptions.\n  // link: myLink\n\n  // Override default cache\n  cache,\n\n  // Override the way the Authorization header is set\n  // getAuth: (tokenName) => ...\n\n  // Additional ApolloClient options\n  // apollo: { ... }\n\n  // Client local data (see apollo-link-state)\n  // clientState: { resolvers: { ... }, defaults: { ... } }\n}\n\n// Call this in the Vue app file\nexport function createClient (options = {}) {\n  const link = onError(error => {\n    logErrorMessages(error)\n    checkNeedLogin(error)\n  })\n\n  // Create apollo client\n  const { apolloClient, wsClient } = createApolloClient({\n    ...defaultOptions,\n    ...options,\n    link,\n  })\n  apolloClient.wsClient = wsClient\n  return apolloClient\n}\n\n// Manually call this when user log in\nexport async function onLogin (apolloClient, token) {\n  if (typeof localStorage !== 'undefined' && token) {\n    localStorage.setItem(AUTH_TOKEN, token)\n  }\n  if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)\n  try {\n    await apolloClient.resetStore()\n  } catch (e) {\n    // eslint-disable-next-line no-console\n    console.log('%cError on cache reset (login)', 'color: orange;', e.message)\n  }\n}\n\n// Manually call this when user log out\nexport async function onLogout (apolloClient) {\n  if (typeof localStorage !== 'undefined') {\n    localStorage.removeItem(AUTH_TOKEN)\n  }\n  if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)\n  try {\n    await apolloClient.resetStore()\n  } catch (e) {\n    // eslint-disable-next-line no-console\n    console.log('%cError on cache reset (logout)', 'color: orange;', e.message)\n  }\n}\n"
  },
  {
    "path": "packages/frontend/tailwind.config.js",
    "content": "module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        'gray-850': '#212935',\n      },\n      spacing: {\n        128: '32rem',\n        192: '48rem',\n        256: '64rem',\n      },\n      inset: theme => ({\n        'full': '100%',\n        ...theme('spacing'),\n      }),\n      maxWidth: theme => ({\n        ...theme('width'),\n      }),\n      maxHeight: theme => ({\n        ...theme('width'),\n      }),\n      opacity: {\n        '10': '0.1',\n        '90': '0.9',\n      },\n    },\n  },\n  variants: {\n    appearance: ['responsive'],\n    backgroundAttachment: ['responsive'],\n    backgroundColor: ['responsive', 'hover', 'focus', 'group-hover'],\n    backgroundPosition: ['responsive'],\n    backgroundRepeat: ['responsive'],\n    backgroundSize: ['responsive'],\n    borderCollapse: [],\n    borderColor: ['responsive', 'hover', 'focus'],\n    borderRadius: ['responsive'],\n    borderStyle: ['responsive'],\n    borderWidth: ['responsive'],\n    cursor: ['responsive'],\n    display: ['responsive', 'group-hover'],\n    flexDirection: ['responsive'],\n    flexWrap: ['responsive'],\n    alignItems: ['responsive'],\n    alignSelf: ['responsive'],\n    justifyContent: ['responsive'],\n    alignContent: ['responsive'],\n    flex: ['responsive'],\n    flexGrow: ['responsive'],\n    flexShrink: ['responsive'],\n    float: ['responsive'],\n    fontFamily: ['responsive'],\n    fontWeight: ['responsive', 'hover', 'focus'],\n    height: ['responsive'],\n    lineHeight: ['responsive'],\n    listStylePosition: ['responsive'],\n    listStyleType: ['responsive'],\n    margin: ['responsive', 'first'],\n    maxHeight: ['responsive'],\n    maxWidth: ['responsive'],\n    minHeight: ['responsive'],\n    minWidth: ['responsive'],\n    negativeMargin: ['responsive'],\n    objectFit: ['responsive'],\n    objectPosition: ['responsive'],\n    opacity: ['responsive', 'hover'],\n    outline: ['focus'],\n    overflow: ['responsive'],\n    padding: ['responsive'],\n    pointerEvents: ['responsive'],\n    position: ['responsive'],\n    inset: ['responsive'],\n    resize: ['responsive'],\n    boxShadow: ['responsive', 'hover', 'focus'],\n    fill: [],\n    stroke: [],\n    tableLayout: ['responsive'],\n    textAlign: ['responsive'],\n    textColor: ['responsive', 'hover', 'focus', 'group-hover'],\n    fontSize: ['responsive'],\n    fontStyle: ['responsive'],\n    textTransform: ['responsive'],\n    textDecoration: ['responsive', 'hover', 'focus'],\n    fontSmoothing: ['responsive'],\n    letterSpacing: ['responsive'],\n    userSelect: ['responsive'],\n    verticalAlign: ['responsive'],\n    visibility: ['responsive', 'hover', 'group-hover'],\n    whitespace: ['responsive'],\n    wordBreak: ['responsive'],\n    width: ['responsive'],\n    zIndex: ['responsive'],\n  },\n  corePlugins: {\n    container: false,\n  },\n  plugins: [],\n}\n"
  },
  {
    "path": "packages/frontend/tests/e2e/.eslintrc.js",
    "content": "module.exports = {\n  plugins: [\n    'cypress',\n  ],\n  env: {\n    mocha: true,\n    'cypress/globals': true,\n  },\n  rules: {\n    strict: 'off',\n  },\n}\n"
  },
  {
    "path": "packages/frontend/tests/e2e/plugins/index.js",
    "content": "// https://docs.cypress.io/guides/guides/plugins-guide.html\n\n// if you need a custom webpack configuration you can uncomment the following import\n// and then use the `file:preprocessor` event\n// as explained in the cypress docs\n// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples\n\n/* eslint-disable import/no-extraneous-dependencies, global-require, arrow-body-style */\n// const webpack = require('@cypress/webpack-preprocessor')\n\nmodule.exports = (on, config) => {\n  // on('file:preprocessor', webpack({\n  //  webpackOptions: require('@vue/cli-service/webpack.config'),\n  //  watchOptions: {}\n  // }))\n\n  return Object.assign({}, config, {\n    fixturesFolder: 'tests/e2e/fixtures',\n    integrationFolder: 'tests/e2e/specs',\n    screenshotsFolder: 'tests/e2e/screenshots',\n    videosFolder: 'tests/e2e/videos',\n    supportFile: 'tests/e2e/support/index.js',\n  })\n}\n"
  },
  {
    "path": "packages/frontend/tests/e2e/specs/test.js",
    "content": "// https://docs.cypress.io/api/introduction/api.html\n\ndescribe('My First Test', () => {\n  it('Visits the app root url', () => {\n    cy.visit('/')\n    cy.contains('h1', 'Welcome to Your Vue.js App')\n  })\n})\n"
  },
  {
    "path": "packages/frontend/tests/e2e/support/commands.js",
    "content": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom commands and overwrite\n// existing commands.\n//\n// For more comprehensive examples of custom\n// commands please read more here:\n// https://on.cypress.io/custom-commands\n// ***********************************************\n//\n//\n// -- This is a parent command --\n// Cypress.Commands.add(\"login\", (email, password) => { ... })\n//\n//\n// -- This is a child command --\n// Cypress.Commands.add(\"drag\", { prevSubject: 'element'}, (subject, options) => { ... })\n//\n//\n// -- This is a dual command --\n// Cypress.Commands.add(\"dismiss\", { prevSubject: 'optional'}, (subject, options) => { ... })\n//\n//\n// -- This is will overwrite an existing command --\n// Cypress.Commands.overwrite(\"visit\", (originalFn, url, options) => { ... })\n"
  },
  {
    "path": "packages/frontend/tests/e2e/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport './commands'\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "packages/frontend/vue.config.js",
    "content": "/** @type {import('@vue/cli-service').ProjectOptions} */\nmodule.exports = {\n  pwa: {\n    name: 'AwesomeJS',\n    themeColor: '#F7DF1D',\n    msTileColor: '#25241F',\n    appleMobileWebAppCapable: 'yes',\n    appleMobileWebAppStatusBarStyle: 'black',\n    iconPaths: {\n      favicon32: 'img/icons/guijs-32.png',\n      favicon16: 'img/icons/guijs-16.png',\n      appleTouchIcon: 'img/icons/guijs-152.png',\n      maskIcon: 'img/icons/guijs-safari-mask.svg',\n      msTileImage: 'img/icons/guijs-144.png',\n    },\n    manifestOptions: {\n      'background_color': '#25241F',\n      'icons': [\n        {\n          'src': 'img/icons/guijs-48.png',\n          'sizes': '48x48',\n          'type': 'image/png',\n        }, {\n          'src': 'img/icons/guijs-72.png',\n          'sizes': '72x72',\n          'type': 'image/png',\n        }, {\n          'src': 'img/icons/guijs-96.png',\n          'sizes': '96x96',\n          'type': 'image/png',\n        }, {\n          'src': 'img/icons/guijs-144.png',\n          'sizes': '144x144',\n          'type': 'image/png',\n        }, {\n          'src': 'img/icons/guijs-168.png',\n          'sizes': '168x168',\n          'type': 'image/png',\n        }, {\n          'src': 'img/icons/guijs-192.png',\n          'sizes': '192x192',\n          'type': 'image/png',\n        }, {\n          'src': 'img/icons/guijs-512.png',\n          'sizes': '512x512',\n          'type': 'image/png',\n        },\n      ],\n    },\n  },\n\n  chainWebpack (config) {\n    config.resolve.symlinks(false)\n\n    config.plugin('prefetch').tap(options => {\n      if (!options[0].fileBlacklist) {\n        options[0].fileBlacklist = []\n      }\n      options[0].fileBlacklist.push(/emoji-toolkit(.)+?\\.js$/)\n      return options\n    })\n  },\n}\n"
  },
  {
    "path": "packages/shared-utils/package.json",
    "content": "{\n  \"name\": \"@awesomejs/shared-utils\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"engines\": {\n    \"node\": \">=10\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc -d\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^3.7.4\"\n  }\n}\n"
  },
  {
    "path": "packages/shared-utils/tags.d.ts",
    "content": "export declare function isSpecialTag(tag: string): boolean;\n"
  },
  {
    "path": "packages/shared-utils/tags.js",
    "content": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nfunction isSpecialTag(tag) {\n    return tag === 'official' ||\n        tag.startsWith('version:');\n}\nexports.isSpecialTag = isSpecialTag;\n"
  },
  {
    "path": "packages/shared-utils/tags.ts",
    "content": "export function isSpecialTag (tag: string) {\n  return tag === 'official' ||\n    tag.startsWith('version:')\n}\n"
  },
  {
    "path": "packages/shared-utils/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"CommonJS\",\n    \"skipLibCheck\": true\n  },\n  \"include\": [\n    \".\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {}\n"
  },
  {
    "path": "workspace.code-workspace",
    "content": "{\n\t\"folders\": [\n\t\t{\n\t\t\t\"path\": \".\"\n\t\t},\n\t\t{\n\t\t\t\"path\": \"packages/frontend\"\n\t\t},\n\t\t{\n\t\t\t\"path\": \"packages/backend\"\n\t\t}\n\t],\n\t\"settings\": {}\n}"
  }
]