├── .circleci └── config.yml ├── .commitlintrc.json ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── doczrc.js ├── lerna.json ├── now.json ├── package.json ├── packages ├── backend │ ├── .eslintignore │ ├── .eslintrc.js │ ├── datamodel.prisma │ ├── docker-compose.yml │ ├── graphqlgen.yml │ ├── nodemon.json │ ├── package.json │ ├── prisma.yml │ ├── src │ │ ├── apolloServer.ts │ │ ├── authentication │ │ │ ├── index.ts │ │ │ └── permissions.ts │ │ ├── context.ts │ │ ├── express.ts │ │ ├── generated │ │ │ ├── graphql │ │ │ │ └── prisma.graphql │ │ │ ├── graphqlgen.ts │ │ │ ├── prisma-client │ │ │ │ ├── index.ts │ │ │ │ └── prisma-schema.ts │ │ │ └── resolvers │ │ │ │ ├── AggregateNews.ts │ │ │ │ ├── AggregateRepo.ts │ │ │ │ ├── AggregateTalk.ts │ │ │ │ ├── Mutation.ts │ │ │ │ ├── News.ts │ │ │ │ ├── NewsConnection.ts │ │ │ │ ├── NewsEdge.ts │ │ │ │ ├── PageInfo.ts │ │ │ │ ├── Query.ts │ │ │ │ ├── Repo.ts │ │ │ │ ├── RepoConnection.ts │ │ │ │ ├── RepoEdge.ts │ │ │ │ ├── Tag.ts │ │ │ │ ├── Talk.ts │ │ │ │ ├── TalkConnection.ts │ │ │ │ ├── TalkEdge.ts │ │ │ │ ├── Upvote.ts │ │ │ │ ├── User.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── makeExecutableSchema.ts │ │ ├── resolvers │ │ │ ├── Mutation.ts │ │ │ ├── News │ │ │ │ ├── AggregateNews.ts │ │ │ │ ├── News.ts │ │ │ │ ├── NewsConnection.ts │ │ │ │ ├── NewsEdge.ts │ │ │ │ └── PageInfo.ts │ │ │ ├── Query.ts │ │ │ ├── Repo │ │ │ │ ├── AggregateRepo.ts │ │ │ │ ├── Repo.ts │ │ │ │ ├── RepoConnection.ts │ │ │ │ └── RepoEdge.ts │ │ │ ├── Tag │ │ │ │ └── Tag.ts │ │ │ ├── Talk │ │ │ │ ├── AggregateTalk.ts │ │ │ │ ├── Talk.ts │ │ │ │ ├── TalkConnection.ts │ │ │ │ └── TalkEdge.ts │ │ │ ├── Upvote │ │ │ │ └── Upvote.ts │ │ │ ├── User │ │ │ │ └── User.ts │ │ │ └── index.ts │ │ └── schema.graphql │ ├── tests │ │ ├── __snapshots__ │ │ │ ├── news.test.ts.snap │ │ │ ├── repo.test.ts.snap │ │ │ ├── talk.test.ts.snap │ │ │ └── upvote.test.ts.snap │ │ ├── mocks │ │ │ └── index.ts │ │ ├── news.test.ts │ │ ├── repo.test.ts │ │ ├── talk.test.ts │ │ ├── upvote.test.ts │ │ └── utils │ │ │ ├── createPrismaMock.ts │ │ │ └── gqlTestClient.ts │ └── tsconfig.json └── frontend │ ├── .babelrc │ ├── .eslintignore │ ├── .eslintrc.js │ ├── codegen.yml │ ├── components │ ├── Button │ │ ├── Button.mdx │ │ ├── Button.test.tsx │ │ ├── Button.tsx │ │ ├── __snapshots__ │ │ │ └── Button.test.tsx.snap │ │ └── index.tsx │ ├── Card │ │ ├── Card.mdx │ │ ├── Card.tsx │ │ ├── NewsCard.tsx │ │ ├── RepoHeroCard.tsx │ │ ├── TalkCard.tsx │ │ ├── TalkInfoSectionCard.tsx │ │ ├── TalkPreviewCard.tsx │ │ └── index.ts │ ├── Footer │ │ ├── Footer.mdx │ │ ├── Footer.test.tsx │ │ ├── Footer.tsx │ │ ├── __snapshots__ │ │ │ └── Footer.test.tsx.snap │ │ └── index.ts │ ├── Header │ │ ├── Header.mdx │ │ ├── Header.tsx │ │ └── index.ts │ ├── Icons │ │ ├── ClockIcon.tsx │ │ ├── GithubIcon.tsx │ │ ├── JsafLogo.tsx │ │ ├── PlayIcon.tsx │ │ ├── TwitterIcon.tsx │ │ └── UpVoteIcon.tsx │ ├── Meta │ │ ├── Meta.test.tsx │ │ ├── Meta.tsx │ │ └── index.tsx │ ├── Page │ │ ├── Page.tsx │ │ └── index.tsx │ ├── Tabs │ │ ├── Tab.tsx │ │ ├── TabDemo.tsx │ │ ├── Tabs.tsx │ │ └── index.ts │ ├── Tags │ │ ├── Tags.test.tsx │ │ ├── Tags.tsx │ │ ├── __snapshots__ │ │ │ └── Tags.test.tsx.snap │ │ └── index.ts │ ├── Typography │ │ ├── Typography.mdx │ │ ├── Typography.test.tsx │ │ ├── Typography.tsx │ │ ├── __snapshots__ │ │ │ └── Typography.test.tsx.snap │ │ └── index.ts │ └── shared │ │ ├── constants.ts │ │ ├── docz-wrapper.tsx │ │ ├── index.tsx │ │ └── theme.ts │ ├── css │ ├── github-markdown-css.css │ └── reach-tooltip.css │ ├── cypress.json │ ├── cypress │ ├── e2e │ │ └── example.spec.ts │ ├── fixtures │ │ └── example.json │ ├── plugins │ │ └── index.js │ ├── support │ │ ├── commands.js │ │ └── index.js │ └── tsconfig.json │ ├── generated │ └── apolloComponents.tsx │ ├── graphql │ └── talks │ │ └── queries │ │ └── talkAggregateQuery.graphql │ ├── index.d.ts │ ├── jest.config.js │ ├── lib │ ├── styled-components.ts │ └── withData.tsx │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── index.tsx │ ├── news.tsx │ ├── repo.tsx │ ├── repos.tsx │ ├── talk.tsx │ ├── talks.tsx │ └── test-tabs.tsx │ ├── static │ └── logo.png │ ├── testUtils │ ├── renderWithTheme.tsx │ └── setupTests.ts │ └── tsconfig.json ├── renovate.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn test 38 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": ["@commitlint/config-conventional"] } 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .docz 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env* 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | 82 | # START MANUAL ADDITIONS 83 | 84 | .DS_Store 85 | 86 | dist/ 87 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "redhat.vscode-yaml", 8 | "Prisma.vscode-graphql", 9 | "dbaeumer.vscode-eslint", 10 | "esbenp.prettier-vscode", 11 | "aaron-bond.better-comments" 12 | ], 13 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 14 | "unwantedRecommendations": [] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "yaml.schemas": { 3 | "http://json.schemastore.org/prisma": "prisma.yml" 4 | }, 5 | "json.schemas": [ 6 | { 7 | "fileMatch": ["renovate.json"], 8 | "url": "http://json.schemastore.org/renovate" 9 | }, 10 | { 11 | "fileMatch": [".eslintrc.json"], 12 | "url": "http://json.schemastore.org/eslintrc" 13 | } 14 | ], 15 | "eslint.validate": [ 16 | { "language": "javascript", "autoFix": true }, 17 | { "language": "javascriptreact", "autoFix": true }, 18 | { "language": "typescript", "autoFix": true }, 19 | { "language": "typescriptreact", "autoFix": true } 20 | ], 21 | "eslint.autoFixOnSave": true, 22 | "eslint.alwaysShowStatus": true, 23 | "editor.formatOnSave": true, 24 | "[javascript]": { 25 | "editor.formatOnSave": false 26 | }, 27 | "[javascriptreact]": { 28 | "editor.formatOnSave": false 29 | }, 30 | "[typescript]": { 31 | "editor.formatOnSave": false 32 | }, 33 | "[typescriptreact]": { 34 | "editor.formatOnSave": false 35 | }, 36 | "prettier.disableLanguages": [ 37 | "javascript", 38 | "javascriptreact", 39 | "typescript", 40 | "typescriptreact" 41 | ], 42 | "eslint.workingDirectories": ["./packages/backend/", "./packages/frontend/"], 43 | "typescript.tsdk": "node_modules/typescript/lib" 44 | } 45 | -------------------------------------------------------------------------------- /doczrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | typescript: true, 3 | baseConfig: 'packages/frontend/components', 4 | title: 'JSUi', 5 | wrapper : '../../components/shared/docz-wrapper.tsx', 6 | propsParser: false, 7 | themeConfig: { 8 | repository: 'https://github.com/javascript-af/javascript-af', 9 | colors: { 10 | primary: '#6200EE', 11 | link: '#6200EE', 12 | }, 13 | logo: { 14 | src: 'https://i.imgur.com/hNUdz1y.png', 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "useWorkspaces": true, 4 | "packages": ["packages/*"], 5 | "version": "0.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [{ "src": "packages/frontend/next.config.js", "use": "@now/next" }], 4 | "routes": [{ "src": "/(.*)", "dest": "packages/frontend/$1" }] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript-af", 3 | "workspaces": { 4 | "packages": [ 5 | "packages/*" 6 | ], 7 | "nohoist": [ 8 | "**/jest-styled-components", 9 | "**/jest-styled-components/**" 10 | ] 11 | }, 12 | "version": "0.0.1", 13 | "main": "index.js", 14 | "repository": "git@github.com:javascript-af/javascript-af.git", 15 | "author": "Harshit Pant ", 16 | "license": "MIT", 17 | "private": true, 18 | "scripts": { 19 | "dev": "lerna run --parallel --scope={@jsaf/backend,@jsaf/frontend} dev", 20 | "test": "lerna run --stream test", 21 | "cm": "commit", 22 | "lint:fe": "cd packages/frontend && yarn lint", 23 | "lint:be": "cd packages/backend && yarn lint", 24 | "lint": "lerna run --parallel --scope={@jsaf/backend,@jsaf/frontend} lint" 25 | }, 26 | "husky": { 27 | "hooks": { 28 | "commit-msg": "commitlint --env HUSKY_GIT_PARAMS", 29 | "pre-commit": "lint-staged" 30 | } 31 | }, 32 | "lint-staged": { 33 | "linters": { 34 | "*.{ts,tsx}": [ 35 | "eslint --fix", 36 | "git add" 37 | ], 38 | "*.{json,yml}": [ 39 | "prettier --write", 40 | "git add" 41 | ] 42 | }, 43 | "ignore": [ 44 | "packages/backend/src/generated/**/*.{ts,tsx}", 45 | "packages/frontend/generated/**/*.{ts,tsx}" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@commitlint/cli": "7.6.1", 50 | "@commitlint/config-conventional": "7.6.0", 51 | "@commitlint/prompt-cli": "7.6.1", 52 | "@typescript-eslint/eslint-plugin": "1.11.0", 53 | "@typescript-eslint/parser": "1.11.0", 54 | "docz": "1.2.0", 55 | "docz-theme-default": "1.2.0", 56 | "eslint": "5.15.3", 57 | "eslint-config-airbnb": "17.1.0", 58 | "eslint-config-airbnb-base": "13.1.0", 59 | "eslint-config-prettier": "4.3.0", 60 | "eslint-import-resolver-typescript": "1.1.1", 61 | "eslint-plugin-cypress": "2.2.1", 62 | "eslint-plugin-import": "2.17.3", 63 | "eslint-plugin-jsx-a11y": "6.2.1", 64 | "eslint-plugin-prettier": "3.1.0", 65 | "eslint-plugin-react": "7.14.0", 66 | "eslint-plugin-react-hooks": "1.6.0", 67 | "husky": "2.4.1", 68 | "lerna": "3.15.0", 69 | "lint-staged": "8.2.1", 70 | "prettier": "1.18.2", 71 | "typescript": "3.5.2" 72 | }, 73 | "dependencies": {} 74 | } 75 | -------------------------------------------------------------------------------- /packages/backend/.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/* 2 | /src/generated/* -------------------------------------------------------------------------------- /packages/backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | parser: '@typescript-eslint/parser', 5 | extends: [ 6 | 'airbnb-base', 7 | 'plugin:@typescript-eslint/recommended', 8 | 'prettier', 9 | 'prettier/@typescript-eslint', 10 | ], 11 | plugins: ['@typescript-eslint', 'import', 'prettier'], 12 | parserOptions: { 13 | ecmaVersion: 2018, 14 | ecmaFeatures: { 15 | impliedStrict: true, 16 | }, 17 | sourceType: 'module', 18 | project: path.resolve(__dirname, './tsconfig.json'), 19 | }, 20 | env: { 21 | es6: true, 22 | jest: true, 23 | node: true, 24 | }, 25 | settings: { 26 | 'import/resolver': { 27 | typescript: { 28 | directory: path.resolve(__dirname, './tsconfig.json'), 29 | }, 30 | }, 31 | }, 32 | rules: { 33 | 'prettier/prettier': 'error', 34 | 'import/no-extraneous-dependencies': [ 35 | 'error', 36 | { 37 | packageDir: [ 38 | path.resolve(__dirname, './'), 39 | path.resolve(__dirname, '../../'), 40 | ], 41 | devDependencies: [ 42 | '**/*.test.js', 43 | '**/*.spec.js', 44 | '/testUtils/**', 45 | '/cypress/**', 46 | ], 47 | }, 48 | ], 49 | 'prefer-destructuring': 'off', 50 | 'no-underscore-dangle': 'off', 51 | '@typescript-eslint/explicit-member-accessibility': 'off', 52 | '@typescript-eslint/explicit-function-return-type': 'off', 53 | '@typescript-eslint/no-explicit-any': 'off', 54 | '@typescript-eslint/interface-name-prefix': ['warn', 'always'], 55 | '@typescript-eslint/no-non-null-assertion': 'off', 56 | '@typescript-eslint/prefer-interface': 'off', 57 | '@typescript-eslint/no-empty-interface': 'off', 58 | '@typescript-eslint/no-unused-vars': [ 59 | 'error', 60 | { 61 | vars: 'local', 62 | args: 'none', 63 | }, 64 | ], 65 | 'import/prefer-default-export': 'off', 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /packages/backend/datamodel.prisma: -------------------------------------------------------------------------------- 1 | type User { 2 | id: ID! @id 3 | name: String! 4 | username: String! @unique 5 | email: String @unique 6 | newsItems: [News] 7 | talks: [Talk] 8 | githubToken: String! 9 | profilePic: String 10 | createdAt: DateTime! @createdAt 11 | updatedAt: DateTime! @updatedAt 12 | } 13 | 14 | type News { 15 | id: ID! @id 16 | slug: String! @unique 17 | title: String! 18 | content: String! 19 | previewImage: String 20 | isFeatured: Boolean 21 | writer: User! @relation(link: INLINE) 22 | tags: [Tag] 23 | createdAt: DateTime! @createdAt 24 | updatedAt: DateTime! @updatedAt 25 | } 26 | 27 | type Talk { 28 | id: ID! @id 29 | slug: String! @unique 30 | title: String! 31 | previewImage: String! 32 | alt: String 33 | isFeatured: Boolean 34 | speaker: User @relation(link: INLINE) 35 | length: Int 36 | tags: [Tag] 37 | createdAt: DateTime! @createdAt 38 | updatedAt: DateTime! @updatedAt 39 | } 40 | 41 | type Repo { 42 | id: ID! @id 43 | slug: String! @unique 44 | githubName: String! 45 | githubOwner: String! 46 | githubUrl: String! 47 | ownerUsername: String! 48 | isFeatured: Boolean 49 | description: String 50 | owner: User @relation(link: INLINE) 51 | tags: [Tag] 52 | upvotes: [Upvote] 53 | createdAt: DateTime! @createdAt 54 | updatedAt: DateTime! @updatedAt 55 | } 56 | 57 | type Upvote { 58 | id: ID! @id 59 | user: User! @relation(link: INLINE) 60 | repo: Repo! @relation(link: INLINE) 61 | createdAt: DateTime! @createdAt 62 | updatedAt: DateTime! @updatedAt 63 | } 64 | 65 | type Tag { 66 | id: ID! @id 67 | name: String! @unique 68 | talks: [Talk] 69 | newsItems: [News] 70 | repos: [Repo] 71 | createdAt: DateTime! @createdAt 72 | updatedAt: DateTime! @updatedAt 73 | } 74 | -------------------------------------------------------------------------------- /packages/backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | prisma: 4 | image: prismagraphql/prisma:1.34 5 | restart: always 6 | ports: 7 | - '4467:4467' 8 | environment: 9 | PRISMA_CONFIG: | 10 | port: 4467 11 | databases: 12 | default: 13 | connector: postgres 14 | host: postgres 15 | port: 5432 16 | user: prisma 17 | rawAccess: true 18 | password: prisma 19 | migrations: true 20 | postgres: 21 | image: postgres 22 | restart: always 23 | environment: 24 | POSTGRES_USER: prisma 25 | POSTGRES_PASSWORD: prisma 26 | volumes: 27 | - postgres:/var/lib/postgresql/data 28 | volumes: 29 | postgres: 30 | -------------------------------------------------------------------------------- /packages/backend/graphqlgen.yml: -------------------------------------------------------------------------------- 1 | # The target programming language for the generated code 2 | language: typescript 3 | 4 | # The file path pointing to your GraphQL schema 5 | schema: ./src/schema.graphql 6 | 7 | # Type definition for the resolver context object 8 | context: ./src/context.ts:IMyContext 9 | 10 | # Map SDL types from the GraphQL schema to TS models 11 | models: 12 | files: 13 | - ./src/generated/prisma-client/index.ts 14 | 15 | # Generated typings for resolvers and default resolver implementations 16 | # Please don't edit this file but just import from here 17 | output: ./src/generated/graphqlgen.ts 18 | 19 | # Temporary scaffolded resolvers to copy and paste in your application 20 | resolver-scaffolding: 21 | output: ./src/generated/resolvers 22 | layout: file-per-type 23 | -------------------------------------------------------------------------------- /packages/backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts", "src/**/*.test.ts"], 5 | "exec": "ts-node ./src/index.ts" 6 | } 7 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jsaf/backend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "nodemon", 8 | "test": "jest", 9 | "codegen": "graphqlgen", 10 | "lint": "eslint --ext .ts .", 11 | "build": "tsc" 12 | }, 13 | "dependencies": { 14 | "apollo-server-express": "^2.3.3", 15 | "cookie-parser": "^1.4.3", 16 | "dotenv": "^8.0.0", 17 | "express": "^4.16.4", 18 | "graphql": "^14.1.1", 19 | "graphql-import": "^0.7.1", 20 | "graphql-middleware": "^3.0.1", 21 | "graphql-shield": "^5.1.0", 22 | "jsonwebtoken": "^8.4.0", 23 | "ms": "^2.1.1", 24 | "passport": "^0.4.0", 25 | "passport-github2": "^0.1.11", 26 | "prisma-client-lib": "^1.25.4" 27 | }, 28 | "devDependencies": { 29 | "@types/cookie-parser": "1.4.1", 30 | "@types/dotenv": "6.1.1", 31 | "@types/express": "4.17.0", 32 | "@types/graphql": "14.2.1", 33 | "@types/jest": "24.0.15", 34 | "@types/jsonwebtoken": "8.3.2", 35 | "@types/ms": "0.7.30", 36 | "@types/node": "10.14.10", 37 | "@types/passport": "1.0.0", 38 | "@types/passport-github2": "1.2.4", 39 | "graphqlgen": "0.6.0-rc9", 40 | "jest": "24.8.0", 41 | "nodemon": "1.19.1", 42 | "ts-jest": "24.0.2", 43 | "ts-node": "8.3.0", 44 | "typescript": "3.5.2" 45 | }, 46 | "jest": { 47 | "preset": "ts-jest", 48 | "testEnvironment": "node", 49 | "testPathIgnorePatterns": [ 50 | "/node_modules/", 51 | "/dist/" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/backend/prisma.yml: -------------------------------------------------------------------------------- 1 | endpoint: ${env:PRISMA_ENDPOINT} 2 | datamodel: datamodel.prisma 3 | secret: ${env:PRISMA_SECRET} 4 | generate: 5 | - generator: typescript-client 6 | output: ./src/generated/prisma-client/ 7 | - generator: graphql-schema 8 | output: ./src/generated/graphql/prisma.graphql 9 | 10 | hooks: 11 | post-deploy: 12 | - graphqlgen 13 | -------------------------------------------------------------------------------- /packages/backend/src/apolloServer.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server-express'; 2 | import makeExecutableSchema from './makeExecutableSchema'; 3 | import { IMyContext } from './context'; 4 | import { Prisma } from './generated/prisma-client'; 5 | 6 | export const db = new Prisma({ 7 | endpoint: process.env.PRISMA_ENDPOINT || 'http://localhost:4466', 8 | secret: process.env.PRISMA_SECRET || '', 9 | }); 10 | 11 | const server = new ApolloServer({ 12 | schema: makeExecutableSchema(), 13 | context: ({ req, res }: any): IMyContext => ({ 14 | req, 15 | res, 16 | prisma: db, 17 | }), 18 | }); 19 | 20 | const graphqlPath = server.graphqlPath; 21 | 22 | export { server as default, graphqlPath }; 23 | -------------------------------------------------------------------------------- /packages/backend/src/authentication/index.ts: -------------------------------------------------------------------------------- 1 | import * as passport from 'passport'; 2 | import { Strategy as GithubStrategy } from 'passport-github2'; 3 | import { db } from '../apolloServer'; 4 | 5 | export const authInit = () => { 6 | passport.use( 7 | new GithubStrategy( 8 | { 9 | clientID: process.env.GITHUB_CLIENT_ID!, 10 | clientSecret: process.env.GITHUB_CLIENT_SECRET!, 11 | passReqToCallback: true, 12 | scope: ['user'], 13 | callbackURL: '/auth/github/callback', 14 | }, 15 | async ( 16 | _: any, 17 | token: string, 18 | __: any, 19 | profile: any, 20 | done: (err: Error | null, user?: any) => void 21 | ) => { 22 | const name = 23 | profile.displayName || profile.username || profile._json.name || ''; 24 | const splitProfileUrl = profile.profileUrl.split('/'); 25 | const fallbackUsername = splitProfileUrl[splitProfileUrl.length - 1]; 26 | const githubUsername = 27 | profile.username || profile._json.login || fallbackUsername; 28 | const email = 29 | (profile.emails && 30 | profile.emails.length > 0 && 31 | profile.emails[0].value) || 32 | null; 33 | try { 34 | const user = await db.upsertUser({ 35 | where: { 36 | username: githubUsername, 37 | }, 38 | create: { 39 | name, 40 | email, 41 | username: githubUsername, 42 | githubToken: token, 43 | profilePic: profile.avatar_url || null, 44 | }, 45 | update: { 46 | githubToken: token, 47 | }, 48 | }); 49 | return done(null, user); 50 | } catch (ex) { 51 | return done(ex); 52 | } 53 | } 54 | ) 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /packages/backend/src/authentication/permissions.ts: -------------------------------------------------------------------------------- 1 | import { rule, shield } from 'graphql-shield'; 2 | import { IMyContext } from '../context'; 3 | 4 | const isAuthenticated = rule()(async (_parent, _args, ctx: IMyContext) => { 5 | return Boolean(ctx.req.user); 6 | }); 7 | 8 | export const permissions = shield({ 9 | Mutation: { 10 | toggleRepoVote: isAuthenticated, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/backend/src/context.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { Prisma, User } from './generated/prisma-client'; 3 | 4 | export interface IRequest extends Request { 5 | userId?: string; // or any other type 6 | user?: User; 7 | } 8 | 9 | export interface IMyContext { 10 | req: IRequest; 11 | res: Response; 12 | prisma: Prisma; 13 | } 14 | -------------------------------------------------------------------------------- /packages/backend/src/express.ts: -------------------------------------------------------------------------------- 1 | import * as cookieParser from 'cookie-parser'; 2 | import * as express from 'express'; 3 | import * as jwt from 'jsonwebtoken'; 4 | import * as passport from 'passport'; 5 | import server, { db } from './apolloServer'; 6 | import { authInit } from './authentication'; 7 | import { IRequest } from './context'; 8 | 9 | import ms = require('ms'); 10 | 11 | const app = express(); 12 | 13 | export interface IUserTokenDecoded { 14 | id: string; 15 | } 16 | 17 | app.use(cookieParser()); 18 | app.use(passport.initialize()); 19 | 20 | authInit(); 21 | 22 | app.use((req: IRequest, _, next) => { 23 | const { token }: { token?: string } = req.cookies; 24 | if (token) { 25 | const decoded = jwt.verify( 26 | token, 27 | process.env.JWT_SECRET! 28 | ) as IUserTokenDecoded; 29 | req.userId = decoded.id; 30 | } 31 | next(); 32 | }); 33 | 34 | /* eslint-disable consistent-return */ 35 | app.use(async (req: IRequest, _, next) => { 36 | // if they aren't logged in, skip this 37 | if (!req.userId) { 38 | return next(); 39 | } 40 | const user = await db.user({ id: req.userId }); 41 | req.user = user; 42 | next(); 43 | }); 44 | /* eslint-enable consistent-return */ 45 | 46 | app.get('/auth/logout', (_req: IRequest, res) => { 47 | res.clearCookie('token'); 48 | res.redirect('/'); 49 | }); 50 | 51 | app.get( 52 | '/auth/github', 53 | passport.authenticate('github', { 54 | scope: ['read:user,user:email'], 55 | session: false, 56 | }) 57 | ); 58 | app.get( 59 | '/auth/github/callback', 60 | passport.authenticate('github', { 61 | session: false, 62 | failureRedirect: '/login?msg=failed', // update later on! 63 | }), 64 | (req, res) => { 65 | res.cookie( 66 | 'token', 67 | jwt.sign({ id: req.user.id }, process.env.JWT_SECRET!, { 68 | expiresIn: '14d', 69 | }), 70 | { 71 | httpOnly: true, 72 | maxAge: ms('14d'), 73 | } 74 | ); 75 | res.redirect('/'); 76 | } 77 | ); 78 | 79 | server.applyMiddleware({ 80 | app, 81 | cors: { 82 | origin: ['http://localhost:3000'], 83 | credentials: true, 84 | }, 85 | }); 86 | 87 | export { app as default }; 88 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/AggregateNews.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { AggregateNewsResolvers } from '../graphqlgen'; 5 | 6 | export const AggregateNews: AggregateNewsResolvers.Type = { 7 | ...AggregateNewsResolvers.defaultResolvers, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/AggregateRepo.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { AggregateRepoResolvers } from '../graphqlgen'; 5 | 6 | export const AggregateRepo: AggregateRepoResolvers.Type = { 7 | ...AggregateRepoResolvers.defaultResolvers, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/AggregateTalk.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { AggregateTalkResolvers } from '../graphqlgen'; 5 | 6 | export const AggregateTalk: AggregateTalkResolvers.Type = { 7 | ...AggregateTalkResolvers.defaultResolvers, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/Mutation.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { MutationResolvers } from '../graphqlgen'; 5 | 6 | export const Mutation: MutationResolvers.Type = { 7 | ...MutationResolvers.defaultResolvers, 8 | toggleRepoVote: (parent, args, ctx) => { 9 | throw new Error('Resolver not implemented'); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/News.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { NewsResolvers } from '../graphqlgen'; 5 | 6 | export const News: NewsResolvers.Type = { 7 | ...NewsResolvers.defaultResolvers, 8 | 9 | writer: (parent, args, ctx) => { 10 | throw new Error('Resolver not implemented'); 11 | }, 12 | tags: (parent, args, ctx) => { 13 | throw new Error('Resolver not implemented'); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/NewsConnection.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { NewsConnectionResolvers } from '../graphqlgen'; 5 | 6 | export const NewsConnection: NewsConnectionResolvers.Type = { 7 | ...NewsConnectionResolvers.defaultResolvers, 8 | 9 | aggregate: (parent, args, ctx) => { 10 | throw new Error('Resolver not implemented'); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/NewsEdge.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { NewsEdgeResolvers } from '../graphqlgen'; 5 | 6 | export const NewsEdge: NewsEdgeResolvers.Type = { 7 | ...NewsEdgeResolvers.defaultResolvers, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/PageInfo.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { PageInfoResolvers } from '../graphqlgen'; 5 | 6 | export const PageInfo: PageInfoResolvers.Type = { 7 | ...PageInfoResolvers.defaultResolvers, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/Query.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { QueryResolvers } from '../graphqlgen'; 5 | 6 | export const Query: QueryResolvers.Type = { 7 | ...QueryResolvers.defaultResolvers, 8 | newsItemBySlug: (parent, args, ctx) => { 9 | throw new Error('Resolver not implemented'); 10 | }, 11 | talkBySlug: (parent, args, ctx) => { 12 | throw new Error('Resolver not implemented'); 13 | }, 14 | repoBySlug: (parent, args, ctx) => { 15 | throw new Error('Resolver not implemented'); 16 | }, 17 | newsConnection: (parent, args, ctx) => { 18 | throw new Error('Resolver not implemented'); 19 | }, 20 | talkConnection: (parent, args, ctx) => { 21 | throw new Error('Resolver not implemented'); 22 | }, 23 | repoConnection: (parent, args, ctx) => { 24 | throw new Error('Resolver not implemented'); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/Repo.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { RepoResolvers } from '../graphqlgen'; 5 | 6 | export const Repo: RepoResolvers.Type = { 7 | ...RepoResolvers.defaultResolvers, 8 | 9 | owner: (parent, args, ctx) => { 10 | throw new Error('Resolver not implemented'); 11 | }, 12 | tags: (parent, args, ctx) => { 13 | throw new Error('Resolver not implemented'); 14 | }, 15 | upvotes: (parent, args, ctx) => { 16 | throw new Error('Resolver not implemented'); 17 | }, 18 | aggregatedUpvotes: (parent, args, ctx) => { 19 | throw new Error('Resolver not implemented'); 20 | }, 21 | userHasVoted: (parent, args, ctx) => { 22 | throw new Error('Resolver not implemented'); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/RepoConnection.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { RepoConnectionResolvers } from '../graphqlgen'; 5 | 6 | export const RepoConnection: RepoConnectionResolvers.Type = { 7 | ...RepoConnectionResolvers.defaultResolvers, 8 | 9 | aggregate: (parent, args, ctx) => { 10 | throw new Error('Resolver not implemented'); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/RepoEdge.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { RepoEdgeResolvers } from '../graphqlgen'; 5 | 6 | export const RepoEdge: RepoEdgeResolvers.Type = { 7 | ...RepoEdgeResolvers.defaultResolvers, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/Tag.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { TagResolvers } from '../graphqlgen'; 5 | 6 | export const Tag: TagResolvers.Type = { 7 | ...TagResolvers.defaultResolvers, 8 | 9 | talks: (parent, args, ctx) => { 10 | throw new Error('Resolver not implemented'); 11 | }, 12 | newsItems: (parent, args, ctx) => { 13 | throw new Error('Resolver not implemented'); 14 | }, 15 | repos: (parent, args, ctx) => { 16 | throw new Error('Resolver not implemented'); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/Talk.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { TalkResolvers } from '../graphqlgen'; 5 | 6 | export const Talk: TalkResolvers.Type = { 7 | ...TalkResolvers.defaultResolvers, 8 | 9 | speaker: (parent, args, ctx) => { 10 | throw new Error('Resolver not implemented'); 11 | }, 12 | tags: (parent, args, ctx) => { 13 | throw new Error('Resolver not implemented'); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/TalkConnection.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { TalkConnectionResolvers } from '../graphqlgen'; 5 | 6 | export const TalkConnection: TalkConnectionResolvers.Type = { 7 | ...TalkConnectionResolvers.defaultResolvers, 8 | 9 | aggregate: (parent, args, ctx) => { 10 | throw new Error('Resolver not implemented'); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/TalkEdge.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { TalkEdgeResolvers } from '../graphqlgen'; 5 | 6 | export const TalkEdge: TalkEdgeResolvers.Type = { 7 | ...TalkEdgeResolvers.defaultResolvers, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/Upvote.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { UpvoteResolvers } from '../graphqlgen'; 5 | 6 | export const Upvote: UpvoteResolvers.Type = { 7 | ...UpvoteResolvers.defaultResolvers, 8 | 9 | user: (parent, args, ctx) => { 10 | throw new Error('Resolver not implemented'); 11 | }, 12 | repo: (parent, args, ctx) => { 13 | throw new Error('Resolver not implemented'); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/User.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { UserResolvers } from '../graphqlgen'; 5 | 6 | export const User: UserResolvers.Type = { 7 | ...UserResolvers.defaultResolvers, 8 | 9 | newsItems: (parent, args, ctx) => { 10 | throw new Error('Resolver not implemented'); 11 | }, 12 | talks: (parent, args, ctx) => { 13 | throw new Error('Resolver not implemented'); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/backend/src/generated/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { Resolvers } from '../graphqlgen'; 5 | 6 | import { Query } from './Query'; 7 | import { News } from './News'; 8 | import { User } from './User'; 9 | import { Talk } from './Talk'; 10 | import { Tag } from './Tag'; 11 | import { Repo } from './Repo'; 12 | import { Upvote } from './Upvote'; 13 | import { NewsConnection } from './NewsConnection'; 14 | import { PageInfo } from './PageInfo'; 15 | import { NewsEdge } from './NewsEdge'; 16 | import { AggregateNews } from './AggregateNews'; 17 | import { TalkConnection } from './TalkConnection'; 18 | import { TalkEdge } from './TalkEdge'; 19 | import { AggregateTalk } from './AggregateTalk'; 20 | import { RepoConnection } from './RepoConnection'; 21 | import { RepoEdge } from './RepoEdge'; 22 | import { AggregateRepo } from './AggregateRepo'; 23 | import { Mutation } from './Mutation'; 24 | 25 | export const resolvers: Resolvers = { 26 | Query, 27 | News, 28 | User, 29 | Talk, 30 | Tag, 31 | Repo, 32 | Upvote, 33 | NewsConnection, 34 | PageInfo, 35 | NewsEdge, 36 | AggregateNews, 37 | TalkConnection, 38 | TalkEdge, 39 | AggregateTalk, 40 | RepoConnection, 41 | RepoEdge, 42 | AggregateRepo, 43 | Mutation, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import expressServer from './express'; 3 | import { graphqlPath } from './apolloServer'; 4 | 5 | const port = process.env.PORT || 4000; 6 | 7 | expressServer.listen(port, () => { 8 | /* eslint-disable no-console */ 9 | console.log(`🚀 Server started at http://localhost:${port}${graphqlPath}`); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/backend/src/makeExecutableSchema.ts: -------------------------------------------------------------------------------- 1 | import { importSchema } from 'graphql-import'; 2 | import { makeExecutableSchema } from 'apollo-server-express'; 3 | import { applyMiddleware } from 'graphql-middleware'; 4 | 5 | import { resolvers } from './resolvers'; 6 | import { permissions } from './authentication/permissions'; 7 | 8 | const typeDefs = importSchema('./src/schema.graphql'); 9 | 10 | export default () => { 11 | const schema = makeExecutableSchema({ 12 | typeDefs, 13 | resolvers: resolvers as any, 14 | }); 15 | return applyMiddleware(schema, permissions); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Mutation.ts: -------------------------------------------------------------------------------- 1 | import { MutationResolvers } from '../generated/graphqlgen'; 2 | 3 | export const Mutation: MutationResolvers.Type = { 4 | ...MutationResolvers.defaultResolvers, 5 | toggleRepoVote: async (_parent, { repoId }, ctx) => { 6 | const { id } = ctx.req.user!; 7 | const alreadyUpvoted = await ctx.prisma.upvotes({ 8 | where: { 9 | user: { 10 | id, 11 | }, 12 | repo: { 13 | id: repoId, 14 | }, 15 | }, 16 | }); 17 | 18 | if (alreadyUpvoted.length > 0) { 19 | return ctx.prisma.deleteUpvote({ id: alreadyUpvoted[0].id }); 20 | } 21 | return ctx.prisma.createUpvote({ 22 | user: { 23 | connect: { 24 | id, 25 | }, 26 | }, 27 | repo: { 28 | connect: { 29 | id: repoId, 30 | }, 31 | }, 32 | }); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/News/AggregateNews.ts: -------------------------------------------------------------------------------- 1 | import { AggregateNewsResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const AggregateNews: AggregateNewsResolvers.Type = { 4 | ...AggregateNewsResolvers.defaultResolvers, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/News/News.ts: -------------------------------------------------------------------------------- 1 | import { NewsResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const News: NewsResolvers.Type = { 4 | ...NewsResolvers.defaultResolvers, 5 | 6 | writer: (parent, _args, ctx) => { 7 | return ctx.prisma.news({ id: parent.id }).writer(); 8 | }, 9 | tags: (parent, _args, ctx) => { 10 | return ctx.prisma.news({ id: parent.id }).tags(); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/News/NewsConnection.ts: -------------------------------------------------------------------------------- 1 | import { NewsConnectionResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const NewsConnection: NewsConnectionResolvers.Type = { 4 | ...NewsConnectionResolvers.defaultResolvers, 5 | aggregate: (_, args, ctx) => { 6 | return ctx.prisma.newsesConnection(args).aggregate(); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/News/NewsEdge.ts: -------------------------------------------------------------------------------- 1 | import { NewsEdgeResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const NewsEdge: NewsEdgeResolvers.Type = { 4 | ...NewsEdgeResolvers.defaultResolvers, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/News/PageInfo.ts: -------------------------------------------------------------------------------- 1 | import { PageInfoResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const PageInfo: PageInfoResolvers.Type = { 4 | ...PageInfoResolvers.defaultResolvers, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Query.ts: -------------------------------------------------------------------------------- 1 | import { QueryResolvers } from '../generated/graphqlgen'; 2 | 3 | export const Query: QueryResolvers.Type = { 4 | ...QueryResolvers.defaultResolvers, 5 | 6 | newsItemBySlug(_, { slug }, { prisma }) { 7 | return prisma.news({ 8 | slug, 9 | }); 10 | }, 11 | newsConnection(_, args, { prisma }) { 12 | return prisma.newsesConnection(args); 13 | }, 14 | 15 | talkConnection(_, args, { prisma }) { 16 | return prisma.talksConnection(args); 17 | }, 18 | talkBySlug(_, { slug }, { prisma }) { 19 | return prisma.talk({ 20 | slug, 21 | }); 22 | }, 23 | 24 | repoConnection(_, args, { prisma }) { 25 | return prisma.repoesConnection(args); 26 | }, 27 | repoBySlug(_, { slug }, { prisma }) { 28 | return prisma.repo({ 29 | slug, 30 | }); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Repo/AggregateRepo.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRepoResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const AggregateRepo: AggregateRepoResolvers.Type = { 4 | ...AggregateRepoResolvers.defaultResolvers, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Repo/Repo.ts: -------------------------------------------------------------------------------- 1 | import { RepoResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const Repo: RepoResolvers.Type = { 4 | ...RepoResolvers.defaultResolvers, 5 | 6 | owner: (parent, _args, ctx) => { 7 | return ctx.prisma.repo({ id: parent.id }).owner(); 8 | }, 9 | tags: (parent, _args, ctx) => { 10 | return ctx.prisma.repo({ id: parent.id }).tags(); 11 | }, 12 | upvotes: (parent, _args, ctx) => { 13 | return ctx.prisma.repo({ id: parent.id }).upvotes(); 14 | }, 15 | aggregatedUpvotes: async (parent, _args, ctx) => { 16 | const result = await ctx.prisma 17 | .upvotesConnection({ where: { repo: { id: parent.id } } }) 18 | .aggregate(); 19 | return result.count; 20 | }, 21 | userHasVoted: (parent, _args, ctx) => { 22 | if (!ctx.req.user) { 23 | return null; 24 | } 25 | // all good here 26 | return ctx.prisma.$exists.upvote({ 27 | user: { 28 | id: ctx.req.user.id, 29 | }, 30 | repo: { 31 | id: parent.id, 32 | }, 33 | }); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Repo/RepoConnection.ts: -------------------------------------------------------------------------------- 1 | import { RepoConnectionResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const RepoConnection: RepoConnectionResolvers.Type = { 4 | ...RepoConnectionResolvers.defaultResolvers, 5 | 6 | aggregate: (_parent, args, ctx) => { 7 | return ctx.prisma.repoesConnection(args).aggregate(); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Repo/RepoEdge.ts: -------------------------------------------------------------------------------- 1 | import { RepoEdgeResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const RepoEdge: RepoEdgeResolvers.Type = { 4 | ...RepoEdgeResolvers.defaultResolvers, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Tag/Tag.ts: -------------------------------------------------------------------------------- 1 | import { TagResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const Tag: TagResolvers.Type = { 4 | ...TagResolvers.defaultResolvers, 5 | 6 | talks: (parent, _args, ctx) => { 7 | return ctx.prisma.tag({ id: parent.id }).talks(); 8 | }, 9 | newsItems: (parent, _args, ctx) => { 10 | return ctx.prisma.tag({ id: parent.id }).newsItems(); 11 | }, 12 | repos: (parent, _args, ctx) => { 13 | return ctx.prisma.tag({ id: parent.id }).repos(); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Talk/AggregateTalk.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { AggregateTalkResolvers } from '../../generated/graphqlgen'; 5 | 6 | export const AggregateTalk: AggregateTalkResolvers.Type = { 7 | ...AggregateTalkResolvers.defaultResolvers, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Talk/Talk.ts: -------------------------------------------------------------------------------- 1 | import { TalkResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const Talk: TalkResolvers.Type = { 4 | ...TalkResolvers.defaultResolvers, 5 | 6 | speaker: (parent, _args, ctx) => { 7 | return ctx.prisma.talk({ id: parent.id }).speaker(); 8 | }, 9 | tags: (parent, _args, ctx) => { 10 | return ctx.prisma.talk({ id: parent.id }).tags(); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Talk/TalkConnection.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { TalkConnectionResolvers } from '../../generated/graphqlgen'; 5 | 6 | export const TalkConnection: TalkConnectionResolvers.Type = { 7 | ...TalkConnectionResolvers.defaultResolvers, 8 | 9 | aggregate: (_parent, args, ctx) => { 10 | return ctx.prisma.talksConnection(args).aggregate(); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Talk/TalkEdge.ts: -------------------------------------------------------------------------------- 1 | // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. 2 | // Please do not import this file directly but copy & paste to your application code. 3 | 4 | import { TalkEdgeResolvers } from '../../generated/graphqlgen'; 5 | 6 | export const TalkEdge: TalkEdgeResolvers.Type = { 7 | ...TalkEdgeResolvers.defaultResolvers, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/Upvote/Upvote.ts: -------------------------------------------------------------------------------- 1 | import { UpvoteResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const Upvote: UpvoteResolvers.Type = { 4 | ...UpvoteResolvers.defaultResolvers, 5 | 6 | user: (parent, _args, ctx) => { 7 | return ctx.prisma.upvote({ id: parent.id }).user(); 8 | }, 9 | repo: (parent, _args, ctx) => { 10 | return ctx.prisma.upvote({ id: parent.id }).repo(); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/User/User.ts: -------------------------------------------------------------------------------- 1 | import { UserResolvers } from '../../generated/graphqlgen'; 2 | 3 | export const User: UserResolvers.Type = { 4 | ...UserResolvers.defaultResolvers, 5 | 6 | newsItems: (parent, args, ctx) => { 7 | return ctx.prisma.user({ id: parent.id }).newsItems(args); 8 | }, 9 | talks: (parent, args, ctx) => { 10 | return ctx.prisma.user({ id: parent.id }).talks(args); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import { Resolvers } from '../generated/graphqlgen'; 2 | 3 | import { Query } from './Query'; 4 | import { News } from './News/News'; 5 | import { User } from './User/User'; 6 | import { NewsConnection } from './News/NewsConnection'; 7 | import { AggregateNews } from './News/AggregateNews'; 8 | import { NewsEdge } from './News/NewsEdge'; 9 | import { PageInfo } from './News/PageInfo'; 10 | import { Talk } from './Talk/Talk'; 11 | import { AggregateTalk } from './Talk/AggregateTalk'; 12 | import { TalkConnection } from './Talk/TalkConnection'; 13 | import { TalkEdge } from './Talk/TalkEdge'; 14 | import { Repo } from './Repo/Repo'; 15 | import { AggregateRepo } from './Repo/AggregateRepo'; 16 | import { RepoConnection } from './Repo/RepoConnection'; 17 | import { RepoEdge } from './Repo/RepoEdge'; 18 | import { Tag } from './Tag/Tag'; 19 | import { Upvote } from './Upvote/Upvote'; 20 | import { Mutation } from './Mutation'; 21 | 22 | export const resolvers: Resolvers = { 23 | Query, 24 | News, 25 | User, 26 | NewsConnection, 27 | AggregateNews, 28 | NewsEdge, 29 | PageInfo, 30 | Talk, 31 | AggregateTalk, 32 | TalkConnection, 33 | TalkEdge, 34 | Repo, 35 | AggregateRepo, 36 | RepoConnection, 37 | RepoEdge, 38 | Tag, 39 | Upvote, 40 | Mutation, 41 | }; 42 | -------------------------------------------------------------------------------- /packages/backend/src/schema.graphql: -------------------------------------------------------------------------------- 1 | # import * from './generated/graphql/prisma.graphql' 2 | type Repo { 3 | id: ID! 4 | slug: String! 5 | githubName: String! 6 | githubOwner: String! 7 | githubUrl: String! 8 | ownerUsername: String! 9 | isFeatured: Boolean 10 | description: String 11 | owner: User 12 | tags( 13 | where: TagWhereInput 14 | orderBy: TagOrderByInput 15 | skip: Int 16 | after: String 17 | before: String 18 | first: Int 19 | last: Int 20 | ): [Tag!] 21 | upvotes( 22 | where: UpvoteWhereInput 23 | orderBy: UpvoteOrderByInput 24 | skip: Int 25 | after: String 26 | before: String 27 | first: Int 28 | last: Int 29 | ): [Upvote!] 30 | aggregatedUpvotes: Int 31 | userHasVoted: Boolean 32 | createdAt: DateTime! 33 | updatedAt: DateTime! 34 | } 35 | 36 | type Query { 37 | newsItemBySlug(slug: String!): News 38 | talkBySlug(slug: String!): Talk 39 | repoBySlug(slug: String!): Repo 40 | newsConnection( 41 | where: NewsWhereInput 42 | orderBy: NewsOrderByInput 43 | skip: Int 44 | after: String 45 | before: String 46 | first: Int 47 | last: Int 48 | ): NewsConnection! 49 | talkConnection( 50 | where: TalkWhereInput 51 | orderBy: TalkOrderByInput 52 | skip: Int 53 | after: String 54 | before: String 55 | first: Int 56 | last: Int 57 | ): TalkConnection! 58 | repoConnection( 59 | where: RepoWhereInput 60 | orderBy: RepoOrderByInput 61 | skip: Int 62 | after: String 63 | before: String 64 | first: Int 65 | last: Int 66 | ): RepoConnection! 67 | } 68 | 69 | type Mutation { 70 | toggleRepoVote(repoId: ID!): Upvote! 71 | } 72 | -------------------------------------------------------------------------------- /packages/backend/tests/__snapshots__/news.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`News Resolvers Get News By Slug 1`] = ` 4 | Object { 5 | "data": Object { 6 | "newsItemBySlug": Object { 7 | "content": "some mdx thing", 8 | "createdAt": "2018-12-21T07:32:22.501Z", 9 | "id": "cjpxpvzai000o0974ydxa80da", 10 | "isFeatured": false, 11 | "previewImage": "s", 12 | "slug": "some-thing", 13 | "tags": Array [ 14 | Object { 15 | "name": "react", 16 | }, 17 | ], 18 | "title": "Some thing", 19 | "updatedAt": "2018-12-21T07:32:22.501Z", 20 | "writer": Object { 21 | "createdAt": "2018-12-21T07:25:09.804Z", 22 | "email": "pantharshit00@gmail.com", 23 | "githubToken": "s", 24 | "id": "cjpxpmpgr000c097495f1kz8w", 25 | "profilePic": "ss", 26 | "updatedAt": "2018-12-21T07:25:09.804Z", 27 | "username": "pantharshit00", 28 | }, 29 | }, 30 | }, 31 | } 32 | `; 33 | 34 | exports[`News Resolvers newsConnection pagination 1`] = ` 35 | Object { 36 | "data": Object { 37 | "newsConnection": Object { 38 | "aggregate": Object { 39 | "count": 2, 40 | }, 41 | "edges": Array [ 42 | Object { 43 | "cursor": "cjq0nc986000j097434ewrq03", 44 | "node": Object { 45 | "id": "cjq0nc986000j097434ewrq03", 46 | "title": "This is a news item", 47 | }, 48 | }, 49 | Object { 50 | "cursor": "cjq0nczi2000t0974i1bprdza", 51 | "node": Object { 52 | "id": "cjq0nczi2000t0974i1bprdza", 53 | "title": "This is a news item second", 54 | }, 55 | }, 56 | ], 57 | "pageInfo": Object { 58 | "endCursor": "cjq0nczi2000t0974i1bprdza", 59 | "hasNextPage": false, 60 | "hasPreviousPage": false, 61 | "startCursor": "cjq0nc986000j097434ewrq03", 62 | }, 63 | }, 64 | }, 65 | } 66 | `; 67 | -------------------------------------------------------------------------------- /packages/backend/tests/__snapshots__/repo.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Repo Resolvers repoBySlug 1`] = ` 4 | Object { 5 | "data": Object { 6 | "repoBySlug": Object { 7 | "createdAt": "2018-12-23T10:24:29.259Z", 8 | "githubName": "IhaveWings", 9 | "githubOwner": "LuciferM", 10 | "id": "cjq0qx0v800050981rha92aq2", 11 | "isFeatured": true, 12 | "owner": Object { 13 | "createdAt": "2018-12-21T07:25:09.804Z", 14 | "email": "iam@thedevil.com", 15 | "githubToken": "s", 16 | "id": "cjpxpmpgr000c097495f1kz8a", 17 | "name": "Lucifer Morningstar", 18 | "profilePic": "ss", 19 | "updatedAt": "2018-12-21T07:25:09.804Z", 20 | "username": "LuciferMorningstar", 21 | }, 22 | "slug": "LuciferM/IhaveWings", 23 | "updatedAt": "2018-12-23T10:24:29.259Z", 24 | }, 25 | }, 26 | } 27 | `; 28 | 29 | exports[`Repo Resolvers repoConnection pagination 1`] = ` 30 | Object { 31 | "data": Object { 32 | "repoConnection": Object { 33 | "aggregate": Object { 34 | "count": 2, 35 | }, 36 | "edges": Array [ 37 | Object { 38 | "cursor": "cjq0qx0v800050981rha92aq2", 39 | "node": Object { 40 | "githubName": "IhaveWings", 41 | "githubOwner": "LuciferM", 42 | "id": "cjq0qx0v800050981rha92aq2", 43 | "owner": Object { 44 | "id": "cjpxpmpgr000c097495f1kz8a", 45 | "name": "Lucifer Morningstar", 46 | }, 47 | }, 48 | }, 49 | Object { 50 | "cursor": "cjq0qx0v800050981rha92qsa", 51 | "node": Object { 52 | "githubName": "WingsAreGone", 53 | "githubOwner": "Amenadiel", 54 | "id": "cjq0qx0v800050981rha92qsa", 55 | "owner": Object { 56 | "id": "cjpxpmpgr000c097495f1kz8a", 57 | "name": "Lucifer Morningstar", 58 | }, 59 | }, 60 | }, 61 | ], 62 | "pageInfo": Object { 63 | "endCursor": "cjq0qx0v800050981rha92qsa", 64 | "hasNextPage": false, 65 | "hasPreviousPage": false, 66 | "startCursor": "cjq0qx0v800050981rha92aq2", 67 | }, 68 | }, 69 | }, 70 | } 71 | `; 72 | -------------------------------------------------------------------------------- /packages/backend/tests/__snapshots__/talk.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Talk Resolvers talkBySlug 1`] = ` 4 | Object { 5 | "data": Object { 6 | "talkBySlug": Object { 7 | "createdAt": "2018-12-23T10:24:29.259Z", 8 | "id": "cjq0qx0v800050981rha92aq1", 9 | "isFeatured": true, 10 | "length": 2500, 11 | "previewImage": "https://google.com", 12 | "slug": "prisma-orm-to-rule-em-all", 13 | "speaker": Object { 14 | "createdAt": "2018-12-22T16:54:51.920Z", 15 | "email": "pantharshit00@gmail.com", 16 | "githubToken": "811c699ef0af73a09ac039fbcf5964da051cb9d9", 17 | "id": "cjpzpf76a002809748lk0c4vm", 18 | "name": "Harshit Pant", 19 | "profilePic": "s", 20 | "updatedAt": "2018-12-23T16:46:23.763Z", 21 | "username": "pantharshit00", 22 | }, 23 | "tags": Array [ 24 | Object { 25 | "name": "react", 26 | }, 27 | ], 28 | "title": "Prisma: ORM to rule em all", 29 | "updatedAt": "2018-12-23T10:24:29.259Z", 30 | }, 31 | }, 32 | } 33 | `; 34 | 35 | exports[`Talk Resolvers talkConnection pagination 1`] = ` 36 | Object { 37 | "data": Object { 38 | "talkConnection": Object { 39 | "aggregate": Object { 40 | "count": 2, 41 | }, 42 | "edges": Array [ 43 | Object { 44 | "cursor": "cjq0qx0v800050981rha92aq1", 45 | "node": Object { 46 | "id": "cjq0qx0v800050981rha92aq1", 47 | "length": null, 48 | "speaker": Object { 49 | "id": "cjpzpf76a002809748lk0c4vm", 50 | "name": "Harshit Pant", 51 | }, 52 | "title": "Prisma: ORM to rule em all", 53 | }, 54 | }, 55 | Object { 56 | "cursor": "cjq0qx0v800050981rha92qsd", 57 | "node": Object { 58 | "id": "cjq0qx0v800050981rha92qsd", 59 | "length": null, 60 | "speaker": Object { 61 | "id": "cjpzpf76a002809748lk0c4vm", 62 | "name": "Harshit Pant", 63 | }, 64 | "title": "ORM ORM ORM", 65 | }, 66 | }, 67 | ], 68 | "pageInfo": Object { 69 | "endCursor": "cjq0qx0v800050981rha92aq1", 70 | "hasNextPage": false, 71 | "hasPreviousPage": false, 72 | "startCursor": "cjq0qx0v800050981rha92aq1", 73 | }, 74 | }, 75 | }, 76 | } 77 | `; 78 | -------------------------------------------------------------------------------- /packages/backend/tests/__snapshots__/upvote.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Upvote Resolvers it should not upvote when not logged in 1`] = ` 4 | Object { 5 | "data": null, 6 | "errors": Array [ 7 | [GraphQLError: Not Authorised!], 8 | ], 9 | } 10 | `; 11 | 12 | exports[`Upvote Resolvers should remove existing upvote from user 1`] = ` 13 | Object { 14 | "data": Object { 15 | "toggleRepoVote": Object { 16 | "id": "someid", 17 | }, 18 | }, 19 | } 20 | `; 21 | 22 | exports[`Upvote Resolvers should upvote the repo if we are logged in and there is no previous upvotes 1`] = ` 23 | Object { 24 | "data": Object { 25 | "toggleRepoVote": Object { 26 | "id": "someid", 27 | }, 28 | }, 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /packages/backend/tests/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export const userMock = jest.fn(() => ({ 2 | id: 'cjpxpmpgr000c097495f1kz8w', 3 | name: 'Harshit', 4 | username: 'pantharshit00', 5 | email: 'pantharshit00@gmail.com', 6 | githubToken: 's', 7 | profilePic: 'ss', 8 | createdAt: '2018-12-21T07:25:09.804Z', 9 | updatedAt: '2018-12-21T07:25:09.804Z', 10 | })); 11 | 12 | export const tagsMock = jest.fn(() => [ 13 | { 14 | name: 'react', 15 | }, 16 | ]); 17 | 18 | export const aggregateMock = jest.fn(() => ({ 19 | count: 2, 20 | })); 21 | 22 | export const speakerMock = jest.fn(() => ({ 23 | id: 'cjpzpf76a002809748lk0c4vm', 24 | name: 'Harshit Pant', 25 | username: 'pantharshit00', 26 | email: 'pantharshit00@gmail.com', 27 | githubToken: '811c699ef0af73a09ac039fbcf5964da051cb9d9', 28 | profilePic: 's', 29 | createdAt: '2018-12-22T16:54:51.920Z', 30 | updatedAt: '2018-12-23T16:46:23.763Z', 31 | })); 32 | 33 | export const ownerMock = jest.fn(() => ({ 34 | id: 'cjpxpmpgr000c097495f1kz8a', 35 | name: 'Lucifer Morningstar', 36 | username: 'LuciferMorningstar', 37 | email: 'iam@thedevil.com', 38 | githubToken: 's', 39 | profilePic: 'ss', 40 | createdAt: '2018-12-21T07:25:09.804Z', 41 | updatedAt: '2018-12-21T07:25:09.804Z', 42 | })); 43 | -------------------------------------------------------------------------------- /packages/backend/tests/news.test.ts: -------------------------------------------------------------------------------- 1 | import { graphqlTestCall } from './utils/gqlTestClient'; 2 | import { prisma } from './utils/createPrismaMock'; 3 | import { userMock, tagsMock, aggregateMock } from './mocks'; 4 | 5 | describe('News Resolvers', () => { 6 | test('Get News By Slug', async () => { 7 | const response = await graphqlTestCall(/* GraphQL */ ` 8 | { 9 | newsItemBySlug(slug: "some-thing") { 10 | id 11 | title 12 | slug 13 | content 14 | previewImage 15 | isFeatured 16 | tags { 17 | name 18 | } 19 | writer { 20 | id 21 | username 22 | email 23 | githubToken 24 | profilePic 25 | createdAt 26 | updatedAt 27 | } 28 | createdAt 29 | updatedAt 30 | } 31 | } 32 | `); 33 | expect(prisma.news).toHaveBeenCalledWith({ slug: 'some-thing' }); 34 | expect(userMock).toHaveBeenCalled(); 35 | expect(tagsMock).toHaveBeenCalled(); 36 | expect(response).toMatchSnapshot(); 37 | }); 38 | 39 | test('newsConnection pagination', async () => { 40 | const response = await graphqlTestCall(/* GraphQL */ ` 41 | { 42 | newsConnection(orderBy: createdAt_DESC) { 43 | pageInfo { 44 | hasNextPage 45 | hasPreviousPage 46 | startCursor 47 | endCursor 48 | } 49 | edges { 50 | node { 51 | id 52 | title 53 | } 54 | cursor 55 | } 56 | aggregate { 57 | count 58 | } 59 | } 60 | } 61 | `); 62 | expect(prisma.newsesConnection).toHaveBeenCalledWith({ 63 | orderBy: 'createdAt_DESC', 64 | }); 65 | expect(aggregateMock).toHaveBeenCalled(); 66 | expect(response).toMatchSnapshot(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /packages/backend/tests/repo.test.ts: -------------------------------------------------------------------------------- 1 | import { graphqlTestCall } from './utils/gqlTestClient'; 2 | import { prisma } from './utils/createPrismaMock'; 3 | import { aggregateMock, ownerMock } from './mocks'; 4 | 5 | describe('Repo Resolvers', () => { 6 | test('repoConnection pagination', async () => { 7 | const response = await graphqlTestCall(/* GraphQL */ ` 8 | { 9 | repoConnection(orderBy: createdAt_DESC) { 10 | pageInfo { 11 | hasNextPage 12 | hasPreviousPage 13 | startCursor 14 | endCursor 15 | } 16 | edges { 17 | node { 18 | id 19 | githubName 20 | githubOwner 21 | owner { 22 | id 23 | name 24 | } 25 | } 26 | cursor 27 | } 28 | aggregate { 29 | count 30 | } 31 | } 32 | } 33 | `); 34 | expect(prisma.repoesConnection).toHaveBeenCalledWith({ 35 | orderBy: 'createdAt_DESC', 36 | }); 37 | expect(aggregateMock).toHaveBeenCalled(); 38 | expect(prisma.repo).toHaveBeenCalledWith({ 39 | id: 'cjq0qx0v800050981rha92aq2', 40 | }); 41 | expect(ownerMock).toHaveBeenCalled(); 42 | expect(response).toMatchSnapshot(); 43 | }); 44 | 45 | test('repoBySlug', async () => { 46 | const response = await graphqlTestCall(/* GraphQL */ ` 47 | { 48 | repoBySlug(slug: "LuciferM/IhaveWings") { 49 | id 50 | slug 51 | githubName 52 | githubOwner 53 | isFeatured 54 | createdAt 55 | updatedAt 56 | owner { 57 | id 58 | name 59 | username 60 | email 61 | githubToken 62 | profilePic 63 | createdAt 64 | updatedAt 65 | } 66 | } 67 | } 68 | `); 69 | expect(prisma.repo).toHaveBeenCalledWith({ 70 | slug: 'LuciferM/IhaveWings', 71 | }); 72 | expect(ownerMock).toHaveBeenCalled(); 73 | expect(response).toMatchSnapshot(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /packages/backend/tests/talk.test.ts: -------------------------------------------------------------------------------- 1 | import { graphqlTestCall } from './utils/gqlTestClient'; 2 | import { prisma } from './utils/createPrismaMock'; 3 | import { aggregateMock, speakerMock } from './mocks'; 4 | 5 | describe('Talk Resolvers', () => { 6 | test('talkConnection pagination', async () => { 7 | const response = await graphqlTestCall(/* GraphQL */ ` 8 | { 9 | talkConnection(orderBy: createdAt_DESC) { 10 | pageInfo { 11 | hasNextPage 12 | hasPreviousPage 13 | startCursor 14 | endCursor 15 | } 16 | edges { 17 | node { 18 | id 19 | title 20 | length 21 | speaker { 22 | id 23 | name 24 | } 25 | } 26 | cursor 27 | } 28 | aggregate { 29 | count 30 | } 31 | } 32 | } 33 | `); 34 | expect(prisma.talksConnection).toHaveBeenCalledWith({ 35 | orderBy: 'createdAt_DESC', 36 | }); 37 | expect(aggregateMock).toHaveBeenCalled(); 38 | expect(prisma.talk).toHaveBeenCalledWith({ 39 | id: 'cjq0qx0v800050981rha92aq1', 40 | }); 41 | expect(speakerMock).toHaveBeenCalled(); 42 | expect(response).toMatchSnapshot(); 43 | }); 44 | 45 | test('talkBySlug', async () => { 46 | const response = await graphqlTestCall(/* GraphQL */ ` 47 | { 48 | talkBySlug(slug: "prisma-orm-to-rule-em-all") { 49 | id 50 | title 51 | slug 52 | previewImage 53 | length 54 | isFeatured 55 | createdAt 56 | updatedAt 57 | tags { 58 | name 59 | } 60 | speaker { 61 | id 62 | name 63 | username 64 | email 65 | githubToken 66 | profilePic 67 | createdAt 68 | updatedAt 69 | } 70 | } 71 | } 72 | `); 73 | expect(prisma.talk).toHaveBeenCalledWith({ 74 | slug: 'prisma-orm-to-rule-em-all', 75 | }); 76 | expect(speakerMock).toHaveBeenCalled(); 77 | expect(response).toMatchSnapshot(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /packages/backend/tests/upvote.test.ts: -------------------------------------------------------------------------------- 1 | import { graphqlTestCall } from './utils/gqlTestClient'; 2 | import { userMock } from './mocks'; 3 | import { prisma } from './utils/createPrismaMock'; 4 | 5 | // authenticated upvote 6 | // unable to upvote when authenticated 7 | // toggleBehaviour 8 | 9 | describe('Upvote Resolvers', () => { 10 | const mutation = /* GraphQL */ ` 11 | mutation { 12 | toggleRepoVote(repoId: "cjq3kf0yq000v0904cqvmnnuf") { 13 | id 14 | } 15 | } 16 | `; 17 | test('it should not upvote when not logged in ', async () => { 18 | const response = await graphqlTestCall(mutation); 19 | expect(response.data).toBeFalsy(); 20 | expect(response.errors).toBeDefined(); 21 | expect(response.errors[0].message).toBe('Not Authorised!'); 22 | expect(response).toMatchSnapshot(); 23 | }); 24 | 25 | test('should upvote the repo if we are logged in and there is no previous upvotes', async () => { 26 | prisma.upvotes = jest.fn(() => []); 27 | const response = await graphqlTestCall(mutation, null, userMock()); 28 | expect(prisma.upvotes).toHaveBeenCalledWith({ 29 | where: { 30 | user: { 31 | id: 'cjpxpmpgr000c097495f1kz8w', 32 | }, 33 | repo: { 34 | id: 'cjq3kf0yq000v0904cqvmnnuf', 35 | }, 36 | }, 37 | }); 38 | expect(prisma.createUpvote).toHaveBeenCalledWith({ 39 | user: { 40 | connect: { 41 | id: 'cjpxpmpgr000c097495f1kz8w', 42 | }, 43 | }, 44 | repo: { 45 | connect: { 46 | id: 'cjq3kf0yq000v0904cqvmnnuf', 47 | }, 48 | }, 49 | }); 50 | expect(response).toMatchSnapshot(); 51 | }); 52 | test('should remove existing upvote from user', async () => { 53 | prisma.upvotes = jest.fn(() => [{ id: 'someid' }]); 54 | const response = await graphqlTestCall(mutation, null, userMock()); 55 | expect(prisma.upvotes).toHaveBeenCalledWith({ 56 | where: { 57 | user: { 58 | id: 'cjpxpmpgr000c097495f1kz8w', 59 | }, 60 | repo: { 61 | id: 'cjq3kf0yq000v0904cqvmnnuf', 62 | }, 63 | }, 64 | }); 65 | 66 | expect(prisma.deleteUpvote).toHaveBeenCalledWith({ 67 | id: 'someid', 68 | }); 69 | expect(response).toMatchSnapshot(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/backend/tests/utils/createPrismaMock.ts: -------------------------------------------------------------------------------- 1 | import { 2 | tagsMock, 3 | userMock, 4 | aggregateMock, 5 | ownerMock, 6 | speakerMock, 7 | } from '../mocks'; 8 | 9 | export const prisma = { 10 | news: jest.fn(() => ({ 11 | id: 'cjpxpvzai000o0974ydxa80da', 12 | title: 'Some thing', 13 | slug: 'some-thing', 14 | content: 'some mdx thing', 15 | previewImage: 's', 16 | isFeatured: false, 17 | createdAt: '2018-12-21T07:32:22.501Z', 18 | updatedAt: '2018-12-21T07:32:22.501Z', 19 | tags: tagsMock, 20 | writer: userMock, 21 | })), 22 | newsesConnection: jest.fn(() => ({ 23 | pageInfo: { 24 | hasNextPage: false, 25 | hasPreviousPage: false, 26 | startCursor: 'cjq0nc986000j097434ewrq03', 27 | endCursor: 'cjq0nczi2000t0974i1bprdza', 28 | }, 29 | edges: [ 30 | { 31 | node: { 32 | id: 'cjq0nc986000j097434ewrq03', 33 | title: 'This is a news item', 34 | }, 35 | cursor: 'cjq0nc986000j097434ewrq03', 36 | }, 37 | { 38 | node: { 39 | id: 'cjq0nczi2000t0974i1bprdza', 40 | title: 'This is a news item second', 41 | }, 42 | cursor: 'cjq0nczi2000t0974i1bprdza', 43 | }, 44 | ], 45 | aggregate: aggregateMock, 46 | })), 47 | repoesConnection: jest.fn(() => ({ 48 | pageInfo: { 49 | hasNextPage: false, 50 | hasPreviousPage: false, 51 | startCursor: 'cjq0qx0v800050981rha92aq2', 52 | endCursor: 'cjq0qx0v800050981rha92qsa', 53 | }, 54 | edges: [ 55 | { 56 | node: { 57 | id: 'cjq0qx0v800050981rha92aq2', 58 | githubName: 'IhaveWings', 59 | githubOwner: 'LuciferM', 60 | }, 61 | cursor: 'cjq0qx0v800050981rha92aq2', 62 | }, 63 | { 64 | node: { 65 | id: 'cjq0qx0v800050981rha92qsa', 66 | githubName: 'WingsAreGone', 67 | githubOwner: 'Amenadiel', 68 | }, 69 | cursor: 'cjq0qx0v800050981rha92qsa', 70 | }, 71 | ], 72 | aggregate: aggregateMock, 73 | })), 74 | repo: jest.fn(() => ({ 75 | id: 'cjq0qx0v800050981rha92aq2', 76 | githubName: 'IhaveWings', 77 | githubOwner: 'LuciferM', 78 | slug: 'LuciferM/IhaveWings', 79 | isFeatured: true, 80 | createdAt: '2018-12-23T10:24:29.259Z', 81 | updatedAt: '2018-12-23T10:24:29.259Z', 82 | owner: ownerMock, 83 | })), 84 | talksConnection: jest.fn(() => ({ 85 | pageInfo: { 86 | hasNextPage: false, 87 | hasPreviousPage: false, 88 | startCursor: 'cjq0qx0v800050981rha92aq1', 89 | endCursor: 'cjq0qx0v800050981rha92aq1', 90 | }, 91 | edges: [ 92 | { 93 | node: { 94 | id: 'cjq0qx0v800050981rha92aq1', 95 | title: 'Prisma: ORM to rule em all', 96 | }, 97 | cursor: 'cjq0qx0v800050981rha92aq1', 98 | }, 99 | { 100 | node: { 101 | id: 'cjq0qx0v800050981rha92qsd', 102 | title: 'ORM ORM ORM', 103 | }, 104 | cursor: 'cjq0qx0v800050981rha92qsd', 105 | }, 106 | ], 107 | aggregate: aggregateMock, 108 | })), 109 | talk: jest.fn(() => ({ 110 | id: 'cjq0qx0v800050981rha92aq1', 111 | title: 'Prisma: ORM to rule em all', 112 | slug: 'prisma-orm-to-rule-em-all', 113 | previewImage: 'https://google.com', 114 | length: 2500, 115 | isFeatured: true, 116 | createdAt: '2018-12-23T10:24:29.259Z', 117 | updatedAt: '2018-12-23T10:24:29.259Z', 118 | speaker: speakerMock, 119 | tags: tagsMock, 120 | })), 121 | createUpvote: jest.fn(() => ({ 122 | id: 'someid', 123 | })), 124 | deleteUpvote: jest.fn(() => ({ 125 | id: 'someid', 126 | })), 127 | upvotes: jest.fn(() => [{ id: 'someid' }]), 128 | }; 129 | -------------------------------------------------------------------------------- /packages/backend/tests/utils/gqlTestClient.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from 'graphql'; 2 | import schema from '../../src/makeExecutableSchema'; 3 | import { prisma } from './createPrismaMock'; 4 | import { User } from '../../src/generated/prisma-client'; 5 | 6 | // add context to this when we have authentication in place 7 | export const graphqlTestCall = async ( 8 | query: any, 9 | variables?: any, 10 | user?: User 11 | ) => { 12 | return graphql( 13 | schema(), 14 | query, 15 | undefined, 16 | { 17 | prisma, 18 | req: { 19 | user, 20 | }, 21 | }, 22 | variables 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": ["dom", "es6", "es2017", "esnext.asynciterable"], 6 | "sourceMap": true, 7 | "outDir": "./dist", 8 | "moduleResolution": "node", 9 | "declaration": false, 10 | 11 | "composite": false, 12 | "removeComments": true, 13 | "noImplicitAny": true, 14 | // "strictNullChecks": true, // This is temporary till we fix some typing. It is related to prisma :D 15 | "strictFunctionTypes": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "allowSyntheticDefaultImports": false, 22 | "emitDecoratorMetadata": true, 23 | "experimentalDecorators": true, 24 | "skipLibCheck": true, 25 | "baseUrl": ".", 26 | "rootDir": "src" 27 | }, 28 | "exclude": ["node_modules", "./src/generated/**/*.ts"], 29 | "include": ["./src/**/*.tsx", "./src/**/*.ts"] 30 | } 31 | -------------------------------------------------------------------------------- /packages/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel", "@zeit/next-typescript/babel"], 3 | "plugins": [["styled-components"]] 4 | } 5 | -------------------------------------------------------------------------------- /packages/frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/* 2 | /generated/* -------------------------------------------------------------------------------- /packages/frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | parser: '@typescript-eslint/parser', 5 | extends: [ 6 | 'plugin:react/recommended', 7 | 'airbnb', 8 | 'plugin:cypress/recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier', 11 | 'prettier/react', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | plugins: [ 15 | 'react', 16 | 'react-hooks', 17 | 'import', 18 | 'cypress', 19 | '@typescript-eslint', 20 | 'prettier', 21 | ], 22 | parserOptions: { 23 | ecmaVersion: 2018, 24 | ecmaFeatures: { 25 | jsx: true, 26 | impliedStrict: true, 27 | }, 28 | sourceType: 'module', 29 | project: path.resolve(__dirname, './tsconfig.json'), 30 | }, 31 | env: { 32 | es6: true, 33 | browser: true, 34 | jest: true, 35 | 'cypress/globals': true, 36 | node: true, 37 | }, 38 | settings: { 39 | react: { 40 | version: 'detect', 41 | }, 42 | 'import/resolver': { 43 | typescript: { 44 | directory: path.resolve(__dirname, './tsconfig.json'), 45 | }, 46 | }, 47 | }, 48 | rules: { 49 | 'prettier/prettier': 'error', 50 | 'react-hooks/rules-of-hooks': 'error', 51 | 'react-hooks/exhaustive-deps': 'warn', 52 | 'react/prop-types': 'off', 53 | 'react/react-in-jsx-scope': 'off', 54 | 'react/jsx-filename-extension': 'off', 55 | 'react/destructuring-assignment': 'off', 56 | 'import/no-extraneous-dependencies': [ 57 | 'error', 58 | { 59 | packageDir: [ 60 | path.resolve(__dirname, './'), 61 | path.resolve(__dirname, '../../'), 62 | ], 63 | devDependencies: [ 64 | '**/*.test.js', 65 | '**/*.spec.js', 66 | '/testUtils/**', 67 | '/cypress/**', 68 | ], 69 | }, 70 | ], 71 | '@typescript-eslint/explicit-member-accessibility': 'off', 72 | '@typescript-eslint/explicit-function-return-type': 'off', 73 | '@typescript-eslint/no-explicit-any': 'off', 74 | '@typescript-eslint/interface-name-prefix': ['warn', 'always'], 75 | '@typescript-eslint/no-non-null-assertion': 'off', 76 | '@typescript-eslint/prefer-interface': 'off', 77 | '@typescript-eslint/no-empty-interface': 'off', 78 | '@typescript-eslint/no-unused-vars': [ 79 | 'error', 80 | { 81 | vars: 'local', 82 | args: 'none', 83 | }, 84 | ], 85 | 'import/prefer-default-export': 'off', 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /packages/frontend/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 'http://localhost:4000/graphql' 3 | documents: 'graphql/**/*.graphql' 4 | generates: 5 | generated/apolloComponents.tsx: 6 | config: 7 | noNamespaces: true 8 | withHooks: true 9 | plugins: 10 | - 'typescript-common' 11 | - 'typescript-client' 12 | - 'typescript-react-apollo' 13 | -------------------------------------------------------------------------------- /packages/frontend/components/Button/Button.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Button 3 | menu: Components 4 | --- 5 | 6 | import { Playground } from 'docz'; 7 | import { Button } from './'; 8 | 9 | # Button 10 | 11 | ## Basic Usage 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/frontend/components/Button/Button.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from './Button'; 3 | import { theme } from '../shared'; 4 | import { renderWithTheme } from '../../testUtils/renderWithTheme'; 5 | import { GithubIcon } from '../Icons/GithubIcon'; 6 | 7 | describe('Button', () => { 8 | test('should render primary button if no props are passed', () => { 9 | const { container } = renderWithTheme(); 10 | expect(container.firstChild).toMatchSnapshot(); 11 | expect(container.firstChild).toHaveTextContent('Sign In'); 12 | expect(container.firstChild).toHaveStyleRule( 13 | 'background-color', 14 | theme.colors.primary.main 15 | ); 16 | }); 17 | test('should render outlined variant', () => { 18 | const { container } = renderWithTheme( 19 | 20 | ); 21 | expect(container.firstChild).toMatchSnapshot(); 22 | expect(container.firstChild).toHaveTextContent('Learn More'); 23 | expect(container.firstChild).toHaveStyleRule('background-color', 'white'); 24 | expect(container.firstChild).toHaveStyleRule( 25 | 'border', 26 | `2px solid ${theme.colors.primary.main}` 27 | ); 28 | }); 29 | test('should render icon variant', () => { 30 | const { container } = renderWithTheme( 31 | 34 | ); 35 | expect(container.firstChild).toMatchSnapshot(); 36 | expect(container.firstChild).toHaveStyleRule('display', 'flex'); 37 | }); 38 | test('should work with the as prop and other styled-system ones', () => { 39 | const { container } = renderWithTheme( 40 | 43 | ); 44 | expect(container.firstChild).toMatchSnapshot(); 45 | expect(container.firstChild).toHaveStyleRule('padding', '8px'); 46 | expect(container.firstChild.nodeName).toBe('A'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/frontend/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | color, 4 | space, 5 | ColorProps, 6 | SpaceProps, 7 | FontFamilyProps, 8 | fontFamily, 9 | fontWeight, 10 | FontWeightProps, 11 | BorderRadiusProps, 12 | borderRadius, 13 | fontSize, 14 | FontSizeProps, 15 | variant, 16 | } from 'styled-system'; 17 | import Ink from 'react-ink'; 18 | import styled from '../../lib/styled-components'; 19 | import { SANS_FAMILY } from '../shared'; 20 | 21 | const applyVariant = variant({ 22 | key: 'buttons', 23 | }); 24 | 25 | const StyledButton = styled.button< 26 | ColorProps & 27 | SpaceProps & 28 | FontWeightProps & 29 | BorderRadiusProps & 30 | FontSizeProps & { variant: string } & React.AnchorHTMLAttributes< 31 | HTMLButtonElement | HTMLAnchorElement 32 | > 33 | >` 34 | position: relative; 35 | border: 0; 36 | display: inline-block; 37 | background: transparent; 38 | cursor: pointer; 39 | font-family: ${SANS_FAMILY}; 40 | padding: 16px; 41 | margin: 8px; 42 | font-size: 1.2rem; 43 | border-radius: 44px; 44 | ${color} 45 | ${space} 46 | ${fontFamily} 47 | ${fontSize} 48 | ${borderRadius} 49 | ${applyVariant} 50 | ${fontWeight} 51 | `; 52 | 53 | export interface IButtonProps 54 | extends SpaceProps, 55 | FontFamilyProps, 56 | BorderRadiusProps, 57 | FontWeightProps, 58 | FontSizeProps, 59 | React.AnchorHTMLAttributes { 60 | variant?: string; 61 | as?: any; 62 | noRipple?: boolean; 63 | } 64 | 65 | export const Button: React.FC = ({ 66 | children, 67 | noRipple, 68 | variant: v = 'primary', 69 | ...others 70 | }) => { 71 | return ( 72 | 73 | {children} 74 | {!noRipple ? : ''} 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /packages/frontend/components/Button/__snapshots__/Button.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Button should render icon variant 1`] = ` 4 | .c0 { 5 | position: relative; 6 | border: 0; 7 | display: inline-block; 8 | background: transparent; 9 | cursor: pointer; 10 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; 11 | padding: 16px; 12 | margin: 8px; 13 | font-size: 1.2rem; 14 | border-radius: 44px; 15 | padding: 8px; 16 | display: -webkit-box; 17 | display: -webkit-flex; 18 | display: -ms-flexbox; 19 | display: flex; 20 | -webkit-align-items: center; 21 | -webkit-box-align: center; 22 | -ms-flex-align: center; 23 | align-items: center; 24 | } 25 | 26 | .c0:hover { 27 | opacity: 0.9; 28 | } 29 | 30 | 57 | `; 58 | 59 | exports[`Button should render outlined variant 1`] = ` 60 | .c0 { 61 | position: relative; 62 | border: 0; 63 | display: inline-block; 64 | background: transparent; 65 | cursor: pointer; 66 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; 67 | padding: 16px; 68 | margin: 8px; 69 | font-size: 1.2rem; 70 | border-radius: 44px; 71 | background-color: white; 72 | border-radius: 44px; 73 | border: 2px solid #452984; 74 | color: #452984; 75 | } 76 | 77 | .c0:hover { 78 | opacity: 0.9; 79 | } 80 | 81 | 91 | `; 92 | 93 | exports[`Button should render primary button if no props are passed 1`] = ` 94 | .c0 { 95 | position: relative; 96 | border: 0; 97 | display: inline-block; 98 | background: transparent; 99 | cursor: pointer; 100 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; 101 | padding: 16px; 102 | margin: 8px; 103 | font-size: 1.2rem; 104 | border-radius: 44px; 105 | color: white; 106 | background-color: #452984; 107 | -webkit-transition: 0.2s background ease; 108 | transition: 0.2s background ease; 109 | } 110 | 111 | .c0:hover { 112 | opacity: 0.9; 113 | background-color: #5D48A7; 114 | } 115 | 116 | 126 | `; 127 | 128 | exports[`Button should work with the as prop and other styled-system ones 1`] = ` 129 | .c0 { 130 | position: relative; 131 | border: 0; 132 | display: inline-block; 133 | background: transparent; 134 | cursor: pointer; 135 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; 136 | padding: 16px; 137 | margin: 8px; 138 | font-size: 1.2rem; 139 | border-radius: 44px; 140 | padding: 8px; 141 | color: white; 142 | background-color: #452984; 143 | -webkit-transition: 0.2s background ease; 144 | transition: 0.2s background ease; 145 | } 146 | 147 | .c0:hover { 148 | opacity: 0.9; 149 | background-color: #5D48A7; 150 | } 151 | 152 | 155 | Just test 156 | 161 | 162 | `; 163 | -------------------------------------------------------------------------------- /packages/frontend/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | -------------------------------------------------------------------------------- /packages/frontend/components/Card/Card.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Card 3 | menu: Components 4 | --- 5 | 6 | import { Playground } from 'docz'; 7 | import { Card, NewsCard, TalkCard, RepoHeroCard, TalkPreviewCard } from './'; 8 | 9 | ## Card 10 | 11 | ### Basic Usage 12 | 13 | 14 |
20 | 25 |
26 |
27 | 28 | ## NewsCard 29 | 30 | ### Basic Usage 31 | 32 | 33 |
39 | 44 |
45 |
46 | 47 | ### Without image 48 | 49 | 50 |
56 | 60 |
61 |
62 | 63 | ## TalksCard 64 | 65 | ### Basic Usage 66 | 67 | 68 |
74 | 84 |
85 |
86 | 87 | ## RepoHeroCard 88 | 89 | ### Basic Usage 90 | 91 | 92 |
98 | 104 |
105 |
106 | 107 | ## TalkPreviewCard 108 | 109 | ### Basic Usage 110 | 111 | 112 |
118 | 128 |
129 |
130 | -------------------------------------------------------------------------------- /packages/frontend/components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { space, SpaceProps } from 'styled-system'; 3 | import styled from '../../lib/styled-components'; 4 | import { Typography } from '../Typography'; 5 | import { Button } from '../Button'; 6 | import { GithubIcon } from '../Icons/GithubIcon'; 7 | import { UpVoteIcon } from '../Icons/UpVoteIcon'; 8 | import { Tags } from '../Tags'; 9 | 10 | const StyledCard = styled.div` 11 | background-color: ${props => props.theme.colors.white}; 12 | box-shadow: 1px 4px 9px rgba(0, 0, 0, 0.16); 13 | border-radius: 44px; 14 | padding: 1rem 2rem; 15 | display: grid; 16 | grid-template-rows: auto auto 1fr auto; 17 | ${space} 18 | `; 19 | 20 | export interface IProps extends SpaceProps { 21 | heading?: string; 22 | tags?: string[]; 23 | content?: string; 24 | } 25 | 26 | export const Paper = StyledCard; 27 | 28 | // hack till https://github.com/styled-components/styled-components/issues/2129 29 | export const handleAs = ( 30 | Comp: React.ComponentType<{ as?: string }> 31 | ): React.FC => ({ innerAs, ...rest }) => ; 32 | 33 | const ViewButton = styled(handleAs(Button))` 34 | display: inline-block; 35 | `; 36 | 37 | export const Card: React.FC = ({ 38 | content = '', 39 | heading = '', 40 | tags = [], 41 | }) => { 42 | return ( 43 | 44 | 45 | 46 | 53 | {heading} 54 | 55 | 56 | 57 | {content} 58 | 59 |
66 | 67 | View 68 | 69 |
74 | 82 | 85 |
86 |
87 |
88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /packages/frontend/components/Card/NewsCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { space, SpaceProps } from 'styled-system'; 3 | import { lighten } from 'polished'; 4 | import styled from '../../lib/styled-components'; 5 | import { Button } from '../Button'; 6 | import { Typography } from '../Typography'; 7 | import { ClockIcon } from '../Icons/ClockIcon'; 8 | import { Tags } from '../Tags'; 9 | 10 | interface IStyledNewsCard extends SpaceProps { 11 | hasImage: boolean; 12 | } 13 | 14 | const StyledNewsCard = styled.div` 15 | background-color: ${props => props.theme.colors.white}; 16 | box-shadow: 1px 4px 9px rgba(0, 0, 0, 0.16); 17 | border-radius: 44px; 18 | display: flex; 19 | flex-direction: column; 20 | overflow: hidden; 21 | ${space}; 22 | img { 23 | width: 100%; 24 | height: 100%; 25 | object-fit: cover; 26 | } 27 | .information { 28 | padding: 1rem 2rem; 29 | display: grid; 30 | grid-template-rows: auto auto 1fr auto; 31 | ${p => !p.hasImage && 'grid-template-rows: auto 1fr auto;'} 32 | height: 100%; 33 | .read-time { 34 | height: 100%; 35 | display: flex; 36 | align-items: center; 37 | svg { 38 | color: ${props => lighten(0.2, props.theme.colors.black)}; 39 | } 40 | } 41 | .infos { 42 | display: flex; 43 | justify-content: space-between; 44 | align-items: center; 45 | } 46 | } 47 | `; 48 | 49 | export interface INewsCardProps extends SpaceProps { 50 | heading: string; 51 | image?: string; 52 | alt?: string; 53 | tags: string[]; 54 | } 55 | 56 | export const NewsCard: React.FC = ({ 57 | heading, 58 | image, 59 | alt, 60 | tags, 61 | ...rest 62 | }) => { 63 | return ( 64 | 65 | {image && ( 66 |
67 | {alt 68 |
69 | )} 70 |
71 | 72 |
73 | 80 | 87 | {heading} 88 | 89 | 90 |
91 |
92 | 95 |
96 | 97 | {/* same fontsize as button text */} 98 | 99 | 3 min 100 | 101 |
102 |
103 |
104 |
105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /packages/frontend/components/Card/RepoHeroCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { space, SpaceProps } from 'styled-system'; 3 | import styled from '../../lib/styled-components'; 4 | import { Typography } from '../Typography'; 5 | import { Button } from '../Button'; 6 | import { GithubIcon } from '../Icons/GithubIcon'; 7 | import { UpVoteIcon } from '../Icons/UpVoteIcon'; 8 | import { Tags } from '../Tags'; 9 | 10 | const StyledCard = styled.div` 11 | background-color: ${props => props.theme.colors.white}; 12 | box-shadow: 1px 4px 9px rgba(0, 0, 0, 0.16); 13 | border-radius: 44px; 14 | padding: 1rem 2rem; 15 | display: grid; 16 | grid-template-rows: auto minmax(5rem, 1fr) auto; 17 | background-size: cover; 18 | background-repeat: no-repeat; 19 | background-image: linear-gradient(to top, rgba(0,0,0,0.4), transparent), 20 | url('${p => p.image}'); 21 | ${space}; 22 | `; 23 | 24 | export interface IRepoHeroCardProps extends SpaceProps { 25 | heading: string; 26 | tags: string[]; 27 | content: string; 28 | image: string; 29 | } 30 | 31 | export const RepoHeroCard: React.FC = ({ 32 | content = '', 33 | heading = '', 34 | tags = [], 35 | image, 36 | ...others 37 | }) => { 38 | return ( 39 | 40 | 41 | 48 | {heading} 49 | 50 | 51 | 61 | {content} 62 | 63 |
70 | 75 |
80 | 88 | 91 |
92 |
93 |
94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /packages/frontend/components/Card/TalkCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { lighten } from 'polished'; 3 | import styled from '../../lib/styled-components'; 4 | import { Typography } from '../Typography'; 5 | import { PlayIcon } from '../Icons/PlayIcon'; 6 | import { ClockIcon } from '../Icons/ClockIcon'; 7 | import { Tags } from '../Tags'; 8 | 9 | const StyledTalksCard = styled.div` 10 | background-color: ${props => props.theme.colors.white}; 11 | box-shadow: 1px 4px 9px rgba(0, 0, 0, 0.16); 12 | border-radius: 44px; 13 | display: flex; 14 | flex-direction: column; 15 | overflow: hidden; 16 | .img-container { 17 | position: relative; 18 | } 19 | img { 20 | width: 100%; 21 | height: 100%; 22 | object-fit: cover; 23 | } 24 | .play-icon { 25 | position: absolute; 26 | /* center the play icon */ 27 | top: 50%; 28 | left: 50%; 29 | transform: translate(-50%, -50%); 30 | } 31 | .information { 32 | padding: 1rem 2rem; 33 | } 34 | .header { 35 | display: flex; 36 | justify-content: space-between; 37 | align-items: center; 38 | padding-bottom: 1rem; 39 | } 40 | .watch-time { 41 | height: 100%; 42 | display: flex; 43 | align-items: center; 44 | svg { 45 | color: ${props => lighten(0.2, props.theme.colors.black)}; 46 | } 47 | } 48 | .info { 49 | display: flex; 50 | justify-content: space-between; 51 | align-items: center; 52 | } 53 | .avatar { 54 | display: flex; 55 | align-items: center; 56 | white-space: nowrap; 57 | } 58 | .avatar img { 59 | border-radius: 50%; 60 | width: 30px; 61 | margin-right: 6px; 62 | } 63 | `; 64 | 65 | export interface ITalkCardProps { 66 | heading: string; 67 | image: string; 68 | alt?: string; 69 | tags: string[]; 70 | avatar: { 71 | name: string; 72 | image: string; 73 | }; 74 | featured?: boolean; 75 | } 76 | 77 | export const TalkCard: React.FC = ({ 78 | image, 79 | alt, 80 | heading, 81 | tags, 82 | avatar, 83 | featured, 84 | ...rest 85 | }) => { 86 | return ( 87 | 88 |
89 | {alt 90 | 91 |
92 |
93 |
94 | 102 | {heading} 103 | 104 |
105 | 106 | 107 | 45 min 108 | 109 |
110 |
111 |
112 | 113 |
114 | {avatar.name} 115 | 116 | {avatar.name} 117 | 118 |
119 |
120 |
121 |
122 | ); 123 | }; 124 | -------------------------------------------------------------------------------- /packages/frontend/components/Card/TalkInfoSectionCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GithubIcon } from '../Icons/GithubIcon'; 3 | import { Button } from '../Button'; 4 | import styled from '../../lib/styled-components'; 5 | import { Typography } from '../Typography'; 6 | import { TwitterIcon } from '../Icons/TwitterIcon'; 7 | 8 | const StyledTalkInfoSectionCard = styled.div` 9 | background-color: ${props => props.theme.colors.white}; 10 | display: flex; 11 | flex-direction: row; 12 | flex-wrap: nowrap; 13 | justify-content: flex-start; 14 | align-content: stretch; 15 | .img { 16 | margin-right: 1.5rem; 17 | img { 18 | width: 200px; 19 | height: 200px; 20 | box-shadow: rgba(0, 0, 0, 0.1) 0px 0.625rem 1.25rem, 21 | rgba(0, 0, 0, 0.12) 0px 0.375rem 0.375rem; 22 | } 23 | } 24 | .info { 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: space-between; 28 | .meta { 29 | display: flex; 30 | justify-content: space-between; 31 | align-items: center; 32 | .icons { 33 | display: flex; 34 | align-items: center; 35 | } 36 | } 37 | p { 38 | word-break: break-word; 39 | max-width: 400px; 40 | } 41 | } 42 | `; 43 | 44 | export interface ITalkInfoSectionCard { 45 | name: string; 46 | image: string; 47 | description: string; 48 | } 49 | 50 | export const TalkInfoSectionCard: React.FC = ({ 51 | name, 52 | image, 53 | description, 54 | }) => { 55 | return ( 56 | 57 |
58 | {name} 59 |
60 |
61 |
62 | 63 | {name} 64 | 65 |
66 | {description} 67 |
68 | 71 |
72 | 75 | 78 |
79 |
80 |
81 |
82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /packages/frontend/components/Card/TalkPreviewCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ITalkCardProps } from './TalkCard'; 3 | import styled from '../../lib/styled-components'; 4 | import { Typography } from '../Typography'; 5 | import { Tags } from '../Tags/Tags'; 6 | 7 | const StyledTalkPreviewCard = styled.div` 8 | background-color: ${props => props.theme.colors.white}; 9 | display: flex; 10 | flex-direction: column; 11 | margin-bottom: 2rem; 12 | .img { 13 | width: 300px; 14 | img { 15 | width: 100%; 16 | height: 100%; 17 | border-top-left-radius: 10%; 18 | border-top-right-radius: 10%; 19 | } 20 | } 21 | `; 22 | 23 | export const TalkPreviewCard: React.FC = props => { 24 | return ( 25 | 26 |
27 | {props.alt} 28 |
29 |
30 | 31 | 38 | {props.heading} 39 | 40 | 45 min 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/frontend/components/Card/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Card'; 2 | export * from './NewsCard'; 3 | export * from './TalkCard'; 4 | export * from './RepoHeroCard'; 5 | export * from './TalkPreviewCard'; 6 | export * from './TalkInfoSectionCard'; 7 | -------------------------------------------------------------------------------- /packages/frontend/components/Footer/Footer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Footer 3 | menu: Components 4 | --- 5 | 6 | import { Playground } from 'docz'; 7 | import { Footer } from './'; 8 | 9 | # Footer 10 | 11 | ## Basic Usage 12 | 13 | 14 |