├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── apps ├── stack-overflow-apis │ ├── .env.example │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── jest.config-e2e.js │ ├── jest.config.js │ ├── nest-cli.json │ ├── ormconfig.ts │ ├── package.json │ ├── src │ │ ├── app.controller.spec.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── app │ │ │ ├── core │ │ │ │ └── interceptors │ │ │ │ │ └── http.exception.ts │ │ │ └── domain │ │ │ │ ├── answers │ │ │ │ ├── answer.controller.ts │ │ │ │ ├── answer.dto.ts │ │ │ │ ├── answer.module.ts │ │ │ │ ├── answer.service.ts │ │ │ │ └── answers.entity.ts │ │ │ │ ├── app.constants.ts │ │ │ │ ├── comments │ │ │ │ ├── comment.controller.ts │ │ │ │ ├── comment.dto.ts │ │ │ │ ├── comment.entity.ts │ │ │ │ ├── comment.module.ts │ │ │ │ └── comment.service.ts │ │ │ │ ├── domain.module.ts │ │ │ │ └── questions │ │ │ │ ├── question.controller.ts │ │ │ │ ├── question.dto.ts │ │ │ │ ├── question.entity.ts │ │ │ │ ├── question.module.ts │ │ │ │ └── question.service.ts │ │ ├── docs │ │ │ ├── swagger.config.ts │ │ │ ├── swagger.interface.ts │ │ │ └── swagger.ts │ │ └── main.ts │ ├── test │ │ ├── app.e2e-spec.ts │ │ └── setEnvVars.js │ ├── tsconfig.build.json │ ├── tsconfig.e2e.json │ └── tsconfig.json └── stack-overflow-ui │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── app.css │ ├── app.tsx │ ├── components │ │ ├── auth │ │ │ ├── login.css │ │ │ └── login.tsx │ │ ├── img │ │ │ └── logo.png │ │ ├── question-with-answers │ │ │ └── overflow.tsx │ │ └── questions │ │ │ ├── css │ │ │ ├── Feed.css │ │ │ ├── Overflow.css │ │ │ ├── OverflowBox.css │ │ │ ├── OverflowHeader.css │ │ │ ├── Post.css │ │ │ ├── Sidebar.css │ │ │ ├── SidebarOptions.css │ │ │ ├── Widget.css │ │ │ └── WidgetContent.css │ │ │ ├── overflow-box.tsx │ │ │ ├── overflow-header.tsx │ │ │ ├── overflow.tsx │ │ │ ├── question.tsx │ │ │ ├── questions-answer-feed.tsx │ │ │ ├── questions-feed.tsx │ │ │ ├── side-bar-options.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── widget-content.tsx │ │ │ └── widget.tsx │ ├── features │ │ ├── answer.slice.ts │ │ ├── question.slice.ts │ │ └── user.slice.ts │ ├── firebase.ts │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── setupTests.ts │ └── store │ │ └── index.ts │ └── tsconfig.json ├── commitlint.config.js ├── docker-compose.override.yml ├── docker-compose.yml ├── docker-utils ├── docker-entrypoint.sh └── entrypoint │ └── init.sh ├── jest.config.js ├── nx.json ├── package.json ├── packages ├── api-auth │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── auth.guard.ts │ │ ├── auth.module.ts │ │ ├── auth.strategy.ts │ │ ├── authz.decorator.ts │ │ ├── firebase.service.ts │ │ ├── index.ts │ │ └── user.ts │ └── tsconfig.json ├── api-config │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── config.default.ts │ │ ├── config.interface.ts │ │ ├── config.module.ts │ │ ├── config.service.ts │ │ └── index.ts │ └── tsconfig.json ├── api-database │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── db.interface.ts │ │ ├── db.module.ts │ │ └── index.ts │ └── tsconfig.json └── api-types │ ├── jest.config.js │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── screens ├── main.png ├── main2.png └── main3.png /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /**/build 3 | **/.next/ 4 | dist 5 | build 6 | ./**/.next -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "commonjs": true 6 | }, 7 | "plugins": [ 8 | "@typescript-eslint" 9 | ], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/eslint-recommended", 13 | "plugin:@typescript-eslint/recommended" 14 | ], 15 | "rules": { 16 | "@typescript-eslint/no-var-requires": "off", 17 | "@typescript-eslint/ban-ts-comment": "off", 18 | "react/react-in-jsx-scope": "off", 19 | "no-use-before-define": [ 20 | "error", 21 | { 22 | "functions": false, 23 | "classes": false, 24 | "variables": false 25 | } 26 | ], 27 | 28 | "@typescript-eslint/no-empty-function": 0, 29 | "@typescript-eslint/no-unused-vars": 0, 30 | "react/no-unknown-property": 0, 31 | "no-console": 0, 32 | "no-undef": 0, 33 | "no-plusplus": 0 34 | } 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | .vercel 10 | .output 11 | /packages/**/build 12 | /packages/**/dist 13 | /apps/**/build 14 | /apps/**/dist 15 | /infra/**/build 16 | /infra/**/dist 17 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | /packages/*/build/** 3 | /packages/*/coverage/** 4 | /node_modules/** 5 | package-lock.json 6 | ./package.json 7 | tmp 8 | dist 9 | */dist 10 | ./**/.next 11 | 12 | *.yml 13 | *.json 14 | *.next 15 | /packages/**/build 16 | /packages/**/dist 17 | /apps/**/build 18 | /apps/**/dist 19 | /infra/**/build 20 | /infra/**/dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Stack Overflow Clone App [Basic] 2 | 3 |

4 | 🥳🥳 stack-overflow-full-stack-clone 🥳🥳 5 |
6 |

7 | 8 |

Overflow clone app made for training on Youtube Javascript.

9 | 10 |

11 | 12 | 13 | 14 | 15 |

16 | 17 |

18 | Key Features • 19 | How To Use • 20 | Download • 21 | Credits • 22 | Related • 23 | License 24 |

25 | 26 | ## Key Features 27 | 28 | 29 | ![](./screens/main.png) 30 | ![](./screens/main2.png) 31 | ![](./screens/main3.png) 32 | 33 | 34 | ## How To Use 35 | 36 | You can go through videos here 37 | https://www.youtube.com/playlist?list=PLIGDNOJWiL19kquPvnT1jn6tnYEiXWpGl 38 | 39 | To clone and run this application, you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. From your command line: 40 | 41 | ```bash 42 | # Clone this repository 43 | $ git clone git@github.com:tkssharma/stack-overflow-full-stack-clone.git 44 | 45 | # Go into the repository 46 | $ cd stack-overflow-full-stack-clone 47 | 48 | # Install dependencies 49 | $ pnpm i 50 | $ docker-compose up 51 | 52 | ``` 53 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=local 2 | PORT=3001 3 | NEW_RELIC_KEY= 4 | DATABASE_URL=postgres://api:development_pass@localhost:5436/overflow-api 5 | 6 | DEBUG=api:* 7 | 8 | SWAGGER_USERNAME=hello 9 | SWAGGER_PASSWORD=hello 10 | 11 | FIREBASE_PRIVATE_KEY="" 12 | FIREBASE_CLIENT_EMAIL=firebase-XXXXXX.com 13 | FIREBASE_DATABASE_URL=https://XXXXX-XXXXXX.com 14 | FIREBASE_PROJECT_ID=XXX-XXXX-50ab6 15 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | .env 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /apps/stack-overflow-apis/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /apps/stack-overflow-apis/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ pnpm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ pnpm run start 40 | 41 | # watch mode 42 | $ pnpm run start:dev 43 | 44 | # production mode 45 | $ pnpm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ pnpm run test 53 | 54 | # e2e tests 55 | $ pnpm run test:e2e 56 | 57 | # test coverage 58 | $ pnpm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/jest.config-e2e.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | setupFiles: ['./test/setEnvVars.js'], 6 | moduleFileExtensions: ['js', 'json', 'ts'], 7 | rootDir: '.', 8 | maxWorkers: 1, 9 | testEnvironment: 'node', 10 | testRegex: '.e2e-spec.ts$', 11 | transform: { 12 | '^.+\\.(t|j)s$': 'ts-jest', 13 | }, 14 | globals: { 15 | 'ts-jest': { 16 | tsconfig: 'tsconfig.e2e.json', 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ['/test/setEnvVars.js'], 3 | silent: false, 4 | moduleFileExtensions: ['js', 'ts'], 5 | rootDir: '.', 6 | testRegex: '[.](spec|test).ts$', 7 | transform: { 8 | '^.+\\.(t|j)s$': 'ts-jest', 9 | }, 10 | coverageDirectory: './coverage', 11 | testEnvironment: 'node', 12 | roots: ['/'], 13 | }; 14 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/ormconfig.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: process.env.DB_DIALECT || 'postgres', 3 | url: process.env.DATABASE_URL, 4 | charset: 'utf8mb4', 5 | synchronize: false, 6 | ssl: 7 | process.env.NODE_ENV !== 'local' && process.env.NODE_ENV !== 'test' 8 | ? { rejectUnauthorized: false } 9 | : false, 10 | logging: true, 11 | entities: ['dist/src/app/domain/**/*.entity.js'], 12 | migrations: ['dist/src/database/migrations/**/*.js'], 13 | subscribers: ['dist/src/database/subscriber/**/*.js'], 14 | cli: { 15 | entitiesDir: 'src/app/domain/**/*.entity.js', 16 | migrationsDir: 'src/migrations', 17 | subscribersDir: 'src/subscriber', 18 | }, 19 | migrationsTransactionMode: 'each', 20 | }; 21 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev/stack-overflow-apis", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json", 21 | "typeorm": "ts-node ./node_modules/typeorm/cli.js", 22 | "typeorm:cli": "ts-node ./node_modules/typeorm/cli -f ./ormconfig.ts", 23 | "migration:generate": "typeorm migration:create -n", 24 | "migration:run": "typeorm migration:run -t each", 25 | "migration:revert": "typeorm migration:revert", 26 | "db:sync": "npm run typeorm:cli schema:sync" 27 | }, 28 | "dependencies": { 29 | "@nestjs/common": "^9.0.0", 30 | "@nestjs/core": "^9.0.0", 31 | "@nestjs/platform-express": "^9.0.0", 32 | "reflect-metadata": "^0.1.13", 33 | "@dev/types":"*", 34 | "@dev/auth":"*", 35 | "@dev/config":"*", 36 | "@dev/database":"*", 37 | "@nestjs/event-emitter": "1.0.0", 38 | "@nestjs/swagger": "^6.1.4", 39 | "@nestjs/terminus": "^9.0.0", 40 | "@nestjs/typeorm": "^9.0.1", 41 | "pg": "^8.7.1", 42 | "rxjs": "^7.2.0", 43 | "typeorm": "^0.3.12", 44 | "url-join-ts": "^1.0.5", 45 | "uuid": "^9.0.0", 46 | "uuidv4": "^6.2.12", 47 | "winston": "^3.3.3", 48 | "swagger-ui-express": "^4.1.6", 49 | "debug": "^4.3.4", 50 | "express": "^4.17.1", 51 | "express-basic-auth": "^1.2.0", 52 | "helmet": "^4.6.0", 53 | "dotenv": "^8.2.0", 54 | "dotenv-cli": "^4.0.0", 55 | "@types/uuid": "^9.0.1", 56 | "bcrypt": "^5.1.0", 57 | "class-transformer": "^0.5.1", 58 | "class-validator": "^0.14.0" 59 | }, 60 | "devDependencies": { 61 | "@nestjs/cli": "^9.0.0", 62 | "@nestjs/schematics": "^9.0.0", 63 | "@nestjs/testing": "^9.0.0", 64 | "@types/express": "^4.17.13", 65 | "@types/jest": "29.2.4", 66 | "@types/node": "18.11.18", 67 | "@types/supertest": "^2.0.11", 68 | "@typescript-eslint/eslint-plugin": "^5.0.0", 69 | "@typescript-eslint/parser": "^5.0.0", 70 | "eslint": "^8.0.1", 71 | "eslint-config-prettier": "^8.3.0", 72 | "eslint-plugin-prettier": "^4.0.0", 73 | "jest": "29.3.1", 74 | "prettier": "^2.3.2", 75 | "source-map-support": "^0.5.20", 76 | "supertest": "^6.1.3", 77 | "ts-jest": "29.0.3", 78 | "ts-loader": "^9.2.3", 79 | "ts-node": "^10.0.0", 80 | "tsconfig-paths": "4.1.1", 81 | "typescript": "^4.7.4" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; 3 | import { 4 | HealthCheck, 5 | HealthCheckService, 6 | TypeOrmHealthIndicator, 7 | } from '@nestjs/terminus'; 8 | 9 | @Controller('/health') 10 | export class AppController { 11 | constructor( 12 | private readonly health: HealthCheckService, 13 | private db: TypeOrmHealthIndicator, 14 | ) {} 15 | 16 | @ApiOkResponse({ description: 'returns the health check ' }) 17 | @ApiTags('health') 18 | @Get() 19 | @HealthCheck() 20 | getHello() { 21 | return this.health.check([async () => this.db.pingCheck('typeorm')]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { DomainModule } from './app/domain/domain.module'; 5 | import { TerminusModule } from '@nestjs/terminus'; 6 | 7 | @Module({ 8 | imports: [DomainModule, TerminusModule], 9 | controllers: [AppController], 10 | providers: [AppService], 11 | }) 12 | export class AppModule {} 13 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/core/interceptors/http.exception.ts: -------------------------------------------------------------------------------- 1 | // Package. 2 | import debug from 'debug'; 3 | import { 4 | ArgumentsHost, 5 | BadRequestException, 6 | Catch, 7 | ExceptionFilter, 8 | HttpException, 9 | HttpStatus, 10 | } from '@nestjs/common'; 11 | 12 | // Internal. 13 | // Code. 14 | @Catch() 15 | export class HttpExceptionFilter implements ExceptionFilter { 16 | public catch(exception: any, host: ArgumentsHost) { 17 | const ctx = host.switchToHttp(); 18 | const response = ctx.getResponse(); 19 | const request = ctx.getRequest(); 20 | const message = 21 | exception instanceof BadRequestException 22 | ? (exception.getResponse() as any)?.message 23 | : exception.message; 24 | 25 | const status = 26 | exception instanceof HttpException 27 | ? exception.getStatus() 28 | : HttpStatus.INTERNAL_SERVER_ERROR; 29 | response.status(status).json({ 30 | status, 31 | message, 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/answers/answer.controller.ts: -------------------------------------------------------------------------------- 1 | // Native. 2 | /* eslint-disable no-useless-escape */ 3 | 4 | // Package. 5 | import { 6 | Body, 7 | Controller, 8 | Get, 9 | HttpCode, 10 | HttpStatus, 11 | Param, 12 | Post, 13 | Put, 14 | Query, 15 | UseGuards, 16 | UsePipes, 17 | ValidationPipe, 18 | } from '@nestjs/common'; 19 | import { 20 | ApiBearerAuth, 21 | ApiConsumes, 22 | ApiCreatedResponse, 23 | ApiForbiddenResponse, 24 | ApiInternalServerErrorResponse, 25 | ApiNotFoundResponse, 26 | ApiOkResponse, 27 | ApiOperation, 28 | ApiTags, 29 | ApiUnprocessableEntityResponse, 30 | } from '@nestjs/swagger'; 31 | import { 32 | BAD_REQUEST, 33 | INTERNAL_SERVER_ERROR, 34 | NO_ENTITY_FOUND, 35 | UNAUTHORIZED_REQUEST, 36 | } from '../app.constants'; 37 | import { AnswerService } from './answer.service'; 38 | import { Type } from 'class-transformer'; 39 | import { query } from 'express'; 40 | import { FirebaseAuthGuard, User, UserMetaData } from '@dev/auth'; 41 | import { 42 | ActionOnAnswerDto, 43 | AnswerBodyDto, 44 | QuestionAnswerByIdDto, 45 | QuestionByIdDto, 46 | } from './answer.dto'; 47 | 48 | @ApiBearerAuth('authorization') 49 | @Controller('questions') 50 | @UsePipes( 51 | new ValidationPipe({ 52 | whitelist: true, 53 | transform: true, 54 | }), 55 | ) 56 | @ApiTags('answers') 57 | export class AnswerController { 58 | constructor(private readonly service: AnswerService) { } 59 | 60 | @HttpCode(HttpStatus.CREATED) 61 | @ApiConsumes('application/json') 62 | @ApiNotFoundResponse({ description: NO_ENTITY_FOUND }) 63 | @ApiForbiddenResponse({ description: UNAUTHORIZED_REQUEST }) 64 | @ApiUnprocessableEntityResponse({ description: BAD_REQUEST }) 65 | @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR }) 66 | @ApiOperation({ 67 | description: 'api should return created questions', 68 | }) 69 | @ApiCreatedResponse({ 70 | description: 'returned created questions successfully', 71 | }) 72 | @UseGuards(FirebaseAuthGuard) 73 | @Post('/:id/answers') 74 | public async createQuestions( 75 | @User() user: UserMetaData, 76 | @Body() body: AnswerBodyDto, 77 | @Param() param: QuestionByIdDto, 78 | ) { 79 | console.log(user); 80 | return await this.service.createAnswerOfQuestion(user, body, param); 81 | } 82 | 83 | @HttpCode(HttpStatus.CREATED) 84 | @ApiConsumes('application/json') 85 | @ApiNotFoundResponse({ description: NO_ENTITY_FOUND }) 86 | @ApiForbiddenResponse({ description: UNAUTHORIZED_REQUEST }) 87 | @ApiUnprocessableEntityResponse({ description: BAD_REQUEST }) 88 | @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR }) 89 | @ApiOperation({ 90 | description: 'api should return created questions', 91 | }) 92 | @ApiCreatedResponse({ 93 | description: 'returned created questions successfully', 94 | }) 95 | @UseGuards(FirebaseAuthGuard) 96 | @Put('/:id/answers/:answer_id') 97 | public async updateQuestionById( 98 | @Query() query: ActionOnAnswerDto, 99 | @Param() param: QuestionAnswerByIdDto, 100 | ) { 101 | return await this.service.updateQuestionById(param, query); 102 | } 103 | 104 | @HttpCode(HttpStatus.OK) 105 | @ApiConsumes('application/json') 106 | @ApiNotFoundResponse({ description: NO_ENTITY_FOUND }) 107 | @ApiForbiddenResponse({ description: UNAUTHORIZED_REQUEST }) 108 | @ApiUnprocessableEntityResponse({ description: BAD_REQUEST }) 109 | @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR }) 110 | @ApiOperation({ 111 | description: 'api should return created questions', 112 | }) 113 | @ApiCreatedResponse({ 114 | description: 'returned created questions successfully', 115 | }) 116 | @Get('/:id/answers') 117 | public async fetchAnswers(@Param() param: QuestionByIdDto) { 118 | return await this.service.fetchAnswers(param); 119 | } 120 | @HttpCode(HttpStatus.OK) 121 | @ApiConsumes('application/json') 122 | @ApiNotFoundResponse({ description: NO_ENTITY_FOUND }) 123 | @ApiForbiddenResponse({ description: UNAUTHORIZED_REQUEST }) 124 | @ApiUnprocessableEntityResponse({ description: BAD_REQUEST }) 125 | @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR }) 126 | @ApiOperation({ 127 | description: 'api should return created questions', 128 | }) 129 | @ApiCreatedResponse({ 130 | description: 'returned created questions successfully', 131 | }) 132 | @Get('/:id/answers') 133 | public async fetchData(@Param() param: QuestionByIdDto) { 134 | return await this.service.fetchAnswers(param); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/answers/answer.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Transform, Type } from 'class-transformer'; 3 | import { 4 | IsDefined, 5 | IsEnum, 6 | IsNumber, 7 | IsOptional, 8 | IsString, 9 | IsUUID, 10 | Min, 11 | MinLength, 12 | } from 'class-validator'; 13 | 14 | export enum ActionType { 15 | 'upvote' = 'upvote', 16 | 'downvote' = 'downvote', 17 | } 18 | 19 | export class ActionOnAnswerDto { 20 | @ApiProperty({ 21 | description: 'action_type', 22 | enum: ActionType, 23 | example: ActionType.upvote, 24 | }) 25 | @IsEnum(ActionType) 26 | @IsDefined() 27 | public action_type!: string; 28 | } 29 | export class QuestionByIdDto { 30 | @ApiProperty({ 31 | description: 'question id', 32 | required: false, 33 | example: '5aed29ed-4e8f-4aa0-ae0f-690038a94926', 34 | }) 35 | @IsUUID() 36 | public id!: string; 37 | } 38 | 39 | 40 | export class QuestionAnswerByIdDto extends QuestionByIdDto { 41 | @ApiProperty({ 42 | description: 'answer_id', 43 | required: false, 44 | example: '5aed29ed-4e8f-4aa0-ae0f-690038a94921', 45 | }) 46 | @IsUUID() 47 | public answer_id!: string; 48 | } 49 | 50 | export class AnswerBodyDto { 51 | @ApiProperty({ 52 | description: 'answer_text [name, description for search', 53 | required: false, 54 | }) 55 | @IsString() 56 | @MinLength(2) 57 | public answer_text!: string; 58 | 59 | @ApiProperty({ 60 | description: 'technology name like java', 61 | required: false, 62 | example: 'java', 63 | }) 64 | @IsOptional() 65 | @MinLength(2) 66 | public comment!: string; 67 | } 68 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/answers/answer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { AnswerController } from './answer.controller'; 4 | import { AnswerService } from './answer.service'; 5 | import { AnswerEntity } from './answers.entity'; 6 | import { QuestionEntity } from '../questions/question.entity'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([AnswerEntity, QuestionEntity])], 10 | controllers: [AnswerController], 11 | providers: [AnswerService], 12 | exports: [AnswerService], 13 | }) 14 | export class AnswerModule {} 15 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/answers/answer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { Like, Repository, Connection, QueryRunner } from 'typeorm'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { debug } from 'debug'; 5 | import { QuestionBodyDto } from '../questions/question.dto'; 6 | import { QuestionEntity } from '../questions/question.entity'; 7 | import { AnswerEntity } from './answers.entity'; 8 | import { 9 | ActionOnAnswerDto, 10 | ActionType, 11 | AnswerBodyDto, 12 | QuestionAnswerByIdDto, 13 | QuestionByIdDto, 14 | } from './answer.dto'; 15 | import { UserMetaData } from '@dev/auth'; 16 | 17 | const verbose = debug('api:verbose:handler'); 18 | const error = debug('api:error:handler'); 19 | 20 | @Injectable() 21 | export class AnswerService { 22 | constructor( 23 | @InjectRepository(QuestionEntity) 24 | private questionRepo: Repository, 25 | @InjectRepository(AnswerEntity) 26 | private answerRepo: Repository, 27 | ) { } 28 | 29 | async updateQuestionById( 30 | param: QuestionAnswerByIdDto, 31 | query: ActionOnAnswerDto, 32 | ) { 33 | try { 34 | const { id, answer_id } = param; 35 | const { action_type } = query; 36 | const question = await this.questionRepo.findOne({ 37 | where: { 38 | id, 39 | }, 40 | }); 41 | if (!question) { 42 | throw new NotFoundException(); 43 | } 44 | const answer = await this.answerRepo.findOne({ 45 | where: { 46 | id: answer_id, 47 | }, 48 | }); 49 | if (!answer) { 50 | throw new NotFoundException(); 51 | } 52 | if (action_type === ActionType.upvote) { 53 | const latestUpVote = answer.upvote + 1; 54 | await this.answerRepo.save({ 55 | id: answer_id, 56 | upvote: latestUpVote 57 | }); 58 | return await this.answerRepo.findOne({ where: { id: answer_id } }) 59 | } 60 | const latestDownVote = answer.downvote + 1; 61 | await this.answerRepo.save({ 62 | id: answer_id, 63 | downvote: latestDownVote, 64 | }); 65 | return await this.answerRepo.findOne({ where: { id: answer_id } }) 66 | } catch (err) { 67 | throw err; 68 | } 69 | } 70 | 71 | async createAnswerOfQuestion( 72 | user: UserMetaData, 73 | body: AnswerBodyDto, 74 | param: QuestionByIdDto, 75 | ) { 76 | try { 77 | const { id } = param; 78 | const question = await this.questionRepo.findOne({ 79 | where: { 80 | id, 81 | }, 82 | }); 83 | if (!question) { 84 | throw new NotFoundException(); 85 | } 86 | 87 | return await this.answerRepo.save({ 88 | user_id: user.uid, 89 | user_metadata: { 90 | email: user.email, 91 | picture: user.picture, 92 | name: user.name 93 | }, 94 | question, 95 | }); 96 | } catch (err) { 97 | throw err; 98 | } 99 | } 100 | 101 | async fetchAnswers(param: QuestionByIdDto) { 102 | try { 103 | const { id } = param; 104 | return await this.questionRepo.findOne({ 105 | where: { 106 | id, 107 | }, 108 | relations: ['answers'], 109 | }); 110 | } catch (err) { 111 | throw err; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/answers/answers.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | CreateDateColumn, 5 | UpdateDateColumn, 6 | Entity, 7 | PrimaryGeneratedColumn, 8 | OneToMany, 9 | JoinColumn, 10 | ManyToOne, 11 | } from 'typeorm'; 12 | import { QuestionEntity } from '../questions/question.entity'; 13 | import { CommentEntity } from '../comments/comment.entity'; 14 | 15 | @Entity('answers') 16 | export class AnswerEntity extends BaseEntity { 17 | @PrimaryGeneratedColumn('uuid') 18 | public id!: string; 19 | 20 | @Column({ type: 'varchar' }) 21 | public answer_text!: string; 22 | 23 | @Column({ type: 'int', default: 0 }) 24 | public upvote!: number; 25 | 26 | @Column({ type: 'int', default: 0 }) 27 | public downvote!: number; 28 | 29 | @Column({ type: 'varchar', default: null }) 30 | public comment!: string; 31 | 32 | @Column({ type: 'varchar', default: null }) 33 | public user_id!: string; 34 | 35 | @Column({ type: 'jsonb', default: null }) 36 | public user_metadata!: any; 37 | 38 | @ManyToOne(() => QuestionEntity, (event) => event.answers) 39 | @JoinColumn({ name: 'question_id', referencedColumnName: 'id' }) 40 | public question!: QuestionEntity; 41 | 42 | @OneToMany(() => CommentEntity, (event) => event.answer) 43 | public comments!: CommentEntity[]; 44 | 45 | @CreateDateColumn({ 46 | type: 'timestamptz', 47 | default: () => 'CURRENT_TIMESTAMP', 48 | select: true, 49 | }) 50 | public created_at!: Date; 51 | 52 | @UpdateDateColumn({ 53 | type: 'timestamptz', 54 | default: () => 'CURRENT_TIMESTAMP', 55 | select: true, 56 | }) 57 | public updated_at!: Date; 58 | 59 | @UpdateDateColumn({ 60 | type: 'timestamptz', 61 | default: () => 'CURRENT_TIMESTAMP', 62 | select: true, 63 | }) 64 | public deleted_at!: Date; 65 | } 66 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/app.constants.ts: -------------------------------------------------------------------------------- 1 | export const ENTITY_FOUND = 'entity was found'; 2 | export const NO_ENTITY_FOUND = 'no entity was found'; 3 | 4 | export const PARAMETERS_FAILED_VALIDATION = 'parameters failed validation'; 5 | 6 | export const ENTITY_CREATED = 'entity was created'; 7 | export const ENTITY_ACCEPTED = 'entity was accepted'; 8 | export const INTERNAL_SERVER_ERROR = 'internal server error occurred'; 9 | export const ENTITY_MODIFIED = 'entity was modified'; 10 | export const ENTITY_DELETED = 'entity was deleted'; 11 | 12 | export const RESULTS_RETURNED = 'results were returned'; 13 | export const USER_NOT_FOUND = 14 | 'Unable to get user from User Entity based on userAuthId'; 15 | export const JKWS_RATE_LIMIT = true; 16 | export const JKWS_CACHE = true; 17 | export const JKWS_REQUESTS_PER_MINUTE = 10; 18 | export const BAD_REQUEST = 'bad request'; 19 | export const UNAUTHORIZED_REQUEST = 'user unauthorized'; 20 | 21 | export const INVALID_AUTH_PROVIDER = 'Not Supported Auth provider'; 22 | export const INVALID_BEARER_TOKEN = 23 | 'Invalid Authorization token - Token does not match Bearer .*'; 24 | export const INVALID_AUTH_TOKEN = 'Invalid Auth Token'; 25 | export const INVALID_AUTH_TOKEN_SOURCE = 26 | 'Invalid Auth Token or invalid source of this token, unable to fetch SigningKey for token'; 27 | export const MISSING_AUTH_HEADER = 'Missing Authorization Header'; 28 | 29 | export const ALLOWED_MIMETYPES = [ 30 | 'image/jpg', //jpeg 31 | 'image/jpeg', //jpeg 32 | 'image/png', //png 33 | 'text/plain', //txt 34 | 'image/svg+xml', 35 | 'application/pdf', //pdf 36 | 'application/msword', //doc 37 | 'application/vnd.ms-powerpoint', //pptx 38 | 'application/vnd.ms-excel', //xls 39 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', //docx 40 | 'application/vnd.openxmlformats-officedocument.presentationml.presentation', //pptx 41 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', //xlsx 42 | ]; 43 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/comments/comment.controller.ts: -------------------------------------------------------------------------------- 1 | // Native. 2 | /* eslint-disable no-useless-escape */ 3 | 4 | // Package. 5 | import { 6 | Body, 7 | Controller, 8 | Get, 9 | HttpCode, 10 | HttpStatus, 11 | Param, 12 | Post, 13 | Put, 14 | Query, 15 | UseGuards, 16 | UsePipes, 17 | ValidationPipe, 18 | } from '@nestjs/common'; 19 | import { 20 | ApiBearerAuth, 21 | ApiConsumes, 22 | ApiCreatedResponse, 23 | ApiForbiddenResponse, 24 | ApiInternalServerErrorResponse, 25 | ApiNotFoundResponse, 26 | ApiOkResponse, 27 | ApiOperation, 28 | ApiTags, 29 | ApiUnprocessableEntityResponse, 30 | } from '@nestjs/swagger'; 31 | import { 32 | BAD_REQUEST, 33 | INTERNAL_SERVER_ERROR, 34 | NO_ENTITY_FOUND, 35 | UNAUTHORIZED_REQUEST, 36 | } from '../app.constants'; 37 | import { CommentService } from './comment.service'; 38 | import { Type } from 'class-transformer'; 39 | import { query } from 'express'; 40 | import { FirebaseAuthGuard, User, UserMetaData } from '@dev/auth'; 41 | import { AnswerByIdDto, CommentBodyDto } from './comment.dto'; 42 | 43 | @ApiBearerAuth('authorization') 44 | @Controller('answers') 45 | @UsePipes( 46 | new ValidationPipe({ 47 | whitelist: true, 48 | transform: true, 49 | }), 50 | ) 51 | @ApiTags('comments') 52 | export class CommentController { 53 | constructor(private readonly service: CommentService) {} 54 | 55 | @HttpCode(HttpStatus.CREATED) 56 | @ApiConsumes('application/json') 57 | @ApiNotFoundResponse({ description: NO_ENTITY_FOUND }) 58 | @ApiForbiddenResponse({ description: UNAUTHORIZED_REQUEST }) 59 | @ApiUnprocessableEntityResponse({ description: BAD_REQUEST }) 60 | @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR }) 61 | @ApiOperation({ 62 | description: 'api should return created questions', 63 | }) 64 | @ApiCreatedResponse({ 65 | description: 'returned created questions successfully', 66 | }) 67 | @UseGuards(FirebaseAuthGuard) 68 | @Post('/:id/comment') 69 | public async createQuestions( 70 | @User() user: UserMetaData, 71 | @Body() body: CommentBodyDto, 72 | @Param() param: AnswerByIdDto, 73 | ) { 74 | return await this.service.createCommentOfAnswer(user, body, param); 75 | } 76 | 77 | @HttpCode(HttpStatus.OK) 78 | @ApiConsumes('application/json') 79 | @ApiNotFoundResponse({ description: NO_ENTITY_FOUND }) 80 | @ApiForbiddenResponse({ description: UNAUTHORIZED_REQUEST }) 81 | @ApiUnprocessableEntityResponse({ description: BAD_REQUEST }) 82 | @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR }) 83 | @ApiOperation({ 84 | description: 'api should return created questions', 85 | }) 86 | @ApiCreatedResponse({ 87 | description: 'returned created questions successfully', 88 | }) 89 | @Get('/:id/comments') 90 | public async fetchComments(@Param() param: AnswerByIdDto) { 91 | return await this.service.fetchAllCommentOfAnswer(param); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/comments/comment.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Transform, Type } from 'class-transformer'; 3 | import { 4 | IsDefined, 5 | IsEnum, 6 | IsNumber, 7 | IsOptional, 8 | IsString, 9 | IsUUID, 10 | Min, 11 | MinLength, 12 | } from 'class-validator'; 13 | 14 | export class AnswerByIdDto { 15 | @ApiProperty({ 16 | description: 'question id', 17 | required: false, 18 | example: '5aed29ed-4e8f-4aa0-ae0f-690038a94926', 19 | }) 20 | @IsUUID() 21 | public id!: string; 22 | } 23 | 24 | export class CommentBodyDto { 25 | @ApiProperty({ 26 | description: 'comment_text [name, description for search', 27 | required: false, 28 | }) 29 | @IsString() 30 | @MinLength(2) 31 | public comment_text!: string; 32 | } 33 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/comments/comment.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | CreateDateColumn, 5 | UpdateDateColumn, 6 | Entity, 7 | PrimaryGeneratedColumn, 8 | OneToMany, 9 | JoinColumn, 10 | ManyToOne, 11 | } from 'typeorm'; 12 | import { AnswerEntity } from '../answers/answers.entity'; 13 | import { QuestionEntity } from '../questions/question.entity'; 14 | 15 | @Entity('comments') 16 | export class CommentEntity extends BaseEntity { 17 | @PrimaryGeneratedColumn('uuid') 18 | public id!: string; 19 | 20 | @Column({ type: 'uuid', select: true }) 21 | public answer_id!: string; 22 | 23 | @Column({ type: 'uuid', default: null }) 24 | public parent_id!: string; 25 | 26 | @Column({ type: 'varchar' }) 27 | public comment_text!: string; 28 | 29 | @Column({ type: 'varchar', default: null }) 30 | public user_id!: string; 31 | 32 | @ManyToOne(() => AnswerEntity, (event) => event.comments) 33 | @JoinColumn({ name: 'answer_id', referencedColumnName: 'id' }) 34 | public answer!: AnswerEntity; 35 | 36 | @ManyToOne(() => CommentEntity) 37 | @JoinColumn({ name: 'parent_id', referencedColumnName: 'id' }) 38 | public parent!: CommentEntity; 39 | 40 | @CreateDateColumn({ 41 | type: 'timestamptz', 42 | default: () => 'CURRENT_TIMESTAMP', 43 | select: true, 44 | }) 45 | public created_at!: Date; 46 | 47 | @UpdateDateColumn({ 48 | type: 'timestamptz', 49 | default: () => 'CURRENT_TIMESTAMP', 50 | select: true, 51 | }) 52 | public updated_at!: Date; 53 | 54 | @UpdateDateColumn({ 55 | type: 'timestamptz', 56 | default: () => 'CURRENT_TIMESTAMP', 57 | select: true, 58 | }) 59 | public deleted_at!: Date; 60 | } 61 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/comments/comment.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { CommentController } from './comment.controller'; 4 | import { CommentService } from './comment.service'; 5 | import { CommentEntity } from './comment.entity'; 6 | import { QuestionEntity } from '../questions/question.entity'; 7 | import { AnswerEntity } from '../answers/answers.entity'; 8 | 9 | @Module({ 10 | imports: [TypeOrmModule.forFeature([AnswerEntity, CommentEntity])], 11 | controllers: [CommentController], 12 | providers: [CommentService], 13 | exports: [CommentService], 14 | }) 15 | export class CommentModule {} 16 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/comments/comment.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { Like, Repository, Connection, QueryRunner } from 'typeorm'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { debug } from 'debug'; 5 | import { QuestionBodyDto } from '../questions/question.dto'; 6 | import { QuestionEntity } from '../questions/question.entity'; 7 | import { AnswerEntity } from '../answers/answers.entity'; 8 | import { CommentEntity } from './comment.entity'; 9 | import { AnswerByIdDto, CommentBodyDto } from './comment.dto'; 10 | import { UserMetaData } from '@dev/auth'; 11 | 12 | const verbose = debug('api:verbose:handler'); 13 | const error = debug('api:error:handler'); 14 | 15 | @Injectable() 16 | export class CommentService { 17 | constructor( 18 | @InjectRepository(CommentEntity) 19 | private commentRepo: Repository, 20 | @InjectRepository(AnswerEntity) 21 | private answerRepo: Repository, 22 | ) {} 23 | 24 | async createCommentOfAnswer( 25 | user: UserMetaData, 26 | body: CommentBodyDto, 27 | param: AnswerByIdDto, 28 | ) { 29 | try { 30 | const { id } = param; 31 | const answer = await this.answerRepo.findOne({ 32 | where: { 33 | id, 34 | }, 35 | }); 36 | if (!answer) { 37 | throw new NotFoundException(); 38 | } 39 | 40 | return await this.commentRepo.save({ 41 | ...body, 42 | user_id: user.uid, 43 | answer, 44 | }); 45 | } catch (err) { 46 | throw err; 47 | } 48 | } 49 | async fetchAllCommentOfAnswer(param: AnswerByIdDto) { 50 | try { 51 | const { id } = param; 52 | return await this.answerRepo.findOne({ 53 | where: { 54 | id, 55 | }, 56 | relations: ['comments'], 57 | }); 58 | } catch (err) { 59 | throw err; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/domain.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TerminusModule } from '@nestjs/terminus'; 3 | import { ConfigModule } from '@dev/config'; 4 | import { EventEmitterModule } from '@nestjs/event-emitter'; 5 | import { DBModule } from '@dev/database'; 6 | import { QuestionEntity } from './questions/question.entity'; 7 | import { CommentEntity } from './comments/comment.entity'; 8 | import { AnswerEntity } from './answers/answers.entity'; 9 | import { QuestionModule } from './questions/question.module'; 10 | import { AuthModule } from '@dev/auth'; 11 | import { AnswerModule } from './answers/answer.module'; 12 | import { CommentModule } from './comments/comment.module'; 13 | 14 | @Module({ 15 | imports: [ 16 | AuthModule, 17 | AnswerModule, 18 | CommentModule, 19 | QuestionModule, 20 | EventEmitterModule.forRoot(), 21 | ConfigModule, 22 | DBModule.forRoot({ 23 | entities: [QuestionEntity, CommentEntity, AnswerEntity], 24 | }), 25 | ], 26 | providers: [], 27 | controllers: [], 28 | }) 29 | export class DomainModule {} 30 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/questions/question.controller.ts: -------------------------------------------------------------------------------- 1 | // Native. 2 | /* eslint-disable no-useless-escape */ 3 | 4 | // Package. 5 | import { 6 | Body, 7 | Controller, 8 | Delete, 9 | Get, 10 | HttpCode, 11 | HttpStatus, 12 | Param, 13 | Post, 14 | Put, 15 | Query, 16 | UseGuards, 17 | UsePipes, 18 | ValidationPipe, 19 | } from '@nestjs/common'; 20 | import { 21 | ApiBearerAuth, 22 | ApiConsumes, 23 | ApiCreatedResponse, 24 | ApiForbiddenResponse, 25 | ApiInternalServerErrorResponse, 26 | ApiNotFoundResponse, 27 | ApiOkResponse, 28 | ApiOperation, 29 | ApiTags, 30 | ApiUnprocessableEntityResponse, 31 | } from '@nestjs/swagger'; 32 | import { 33 | BAD_REQUEST, 34 | INTERNAL_SERVER_ERROR, 35 | NO_ENTITY_FOUND, 36 | UNAUTHORIZED_REQUEST, 37 | } from '../app.constants'; 38 | import { QuestionService } from './question.service'; 39 | import { Type } from 'class-transformer'; 40 | import { query } from 'express'; 41 | import { 42 | QuestionBodyDto, 43 | SearchParamDto, 44 | UpdateQuestionBody, 45 | } from './question.dto'; 46 | import { FirebaseAuthGuard, User, UserMetaData } from '@dev/auth'; 47 | import { QuestionByIdDto } from '../answers/answer.dto'; 48 | 49 | @ApiBearerAuth('authorization') 50 | @Controller('questions') 51 | @UsePipes( 52 | new ValidationPipe({ 53 | whitelist: true, 54 | transform: true, 55 | }), 56 | ) 57 | @ApiTags('questions') 58 | export class QuestionController { 59 | constructor(private readonly service: QuestionService) {} 60 | 61 | @HttpCode(HttpStatus.OK) 62 | @ApiConsumes('application/json') 63 | @ApiNotFoundResponse({ description: NO_ENTITY_FOUND }) 64 | @ApiForbiddenResponse({ description: UNAUTHORIZED_REQUEST }) 65 | @ApiUnprocessableEntityResponse({ description: BAD_REQUEST }) 66 | @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR }) 67 | @ApiOperation({ 68 | description: 'api should return all questions', 69 | }) 70 | @ApiOkResponse({ 71 | description: 'returned questions successfully', 72 | }) 73 | @Get('') 74 | public async getAllQuestions(@Query() query: SearchParamDto) { 75 | return await this.service.getAllQuestions(query); 76 | } 77 | 78 | @HttpCode(HttpStatus.CREATED) 79 | @ApiConsumes('application/json') 80 | @ApiNotFoundResponse({ description: NO_ENTITY_FOUND }) 81 | @ApiForbiddenResponse({ description: UNAUTHORIZED_REQUEST }) 82 | @ApiUnprocessableEntityResponse({ description: BAD_REQUEST }) 83 | @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR }) 84 | @ApiOperation({ 85 | description: 'api should return created questions', 86 | }) 87 | @ApiCreatedResponse({ 88 | description: 'returned created questions successfully', 89 | }) 90 | @UseGuards(FirebaseAuthGuard) 91 | @Post('') 92 | public async createQuestions( 93 | @Body() body: QuestionBodyDto, 94 | @User() user: UserMetaData, 95 | ) { 96 | console.log(user); 97 | return await this.service.createQuestion(body, user); 98 | } 99 | 100 | @HttpCode(HttpStatus.CREATED) 101 | @ApiConsumes('application/json') 102 | @ApiNotFoundResponse({ description: NO_ENTITY_FOUND }) 103 | @ApiForbiddenResponse({ description: UNAUTHORIZED_REQUEST }) 104 | @ApiUnprocessableEntityResponse({ description: BAD_REQUEST }) 105 | @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR }) 106 | @ApiOperation({ 107 | description: 'api should return created questions', 108 | }) 109 | @ApiCreatedResponse({ 110 | description: 'returned created questions successfully', 111 | }) 112 | @UseGuards(FirebaseAuthGuard) 113 | @Put('/:id') 114 | public async updateQuestions( 115 | @Body() body: UpdateQuestionBody, 116 | @User() user: UserMetaData, 117 | @Param() param: QuestionByIdDto, 118 | ) { 119 | console.log(user); 120 | return await this.service.updateQuestion(body, user, param); 121 | } 122 | 123 | @HttpCode(HttpStatus.OK) 124 | @ApiConsumes('application/json') 125 | @ApiNotFoundResponse({ description: NO_ENTITY_FOUND }) 126 | @ApiForbiddenResponse({ description: UNAUTHORIZED_REQUEST }) 127 | @ApiUnprocessableEntityResponse({ description: BAD_REQUEST }) 128 | @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR }) 129 | @ApiOperation({ 130 | description: 'api should return with responses questions', 131 | }) 132 | @ApiCreatedResponse({ 133 | description: 'returned created questions with responses successfully', 134 | }) 135 | @Get('/:id') 136 | public async getQuestionById(@Param() param: QuestionByIdDto) { 137 | return await this.service.getQuestionById(param); 138 | } 139 | 140 | @HttpCode(HttpStatus.NO_CONTENT) 141 | @ApiConsumes('application/json') 142 | @ApiNotFoundResponse({ description: NO_ENTITY_FOUND }) 143 | @ApiForbiddenResponse({ description: UNAUTHORIZED_REQUEST }) 144 | @ApiUnprocessableEntityResponse({ description: BAD_REQUEST }) 145 | @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR }) 146 | @ApiOperation({ 147 | description: 'api should return created questions', 148 | }) 149 | @ApiCreatedResponse({ 150 | description: 'returned created questions successfully', 151 | }) 152 | @UseGuards(FirebaseAuthGuard) 153 | @Delete('/:id') 154 | public async deleteQuestions( 155 | @User() user: UserMetaData, 156 | @Param() param: QuestionByIdDto, 157 | ) { 158 | console.log(user); 159 | return await this.service.deleteQuestion(user, param); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/questions/question.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, PartialType } from '@nestjs/swagger'; 2 | import { Transform, Type } from 'class-transformer'; 3 | import { 4 | IsNumber, 5 | IsOptional, 6 | IsString, 7 | Min, 8 | MinLength, 9 | } from 'class-validator'; 10 | 11 | export class QuestionBodyDto { 12 | @ApiProperty({ 13 | description: 'question_text [name, description for search', 14 | required: false, 15 | example: "Why useState hooks are npt working with latest react route change" 16 | }) 17 | @IsString() 18 | @MinLength(2) 19 | public questions_text!: string; 20 | 21 | @ApiProperty({ 22 | description: 'tags for creating question', 23 | required: false, 24 | example: 'java, node_js, react', 25 | }) 26 | @IsString() 27 | @MinLength(2) 28 | public tags!: string; 29 | 30 | @ApiProperty({ 31 | description: 'url for info', 32 | required: false, 33 | example: 'https://stackoverflow.com/questions/50493011/react-ui-router-test-state-change', 34 | }) 35 | @IsString() 36 | @MinLength(2) 37 | public url!: string; 38 | 39 | @ApiProperty({ 40 | description: 'image upload url', 41 | required: false, 42 | example: 'https://cdn-media-1.freecodecamp.org/images/1*TKvlTeNqtkp1s-eVB5Hrvg@2x.png', 43 | }) 44 | @IsString() 45 | @MinLength(2) 46 | public image!: string; 47 | 48 | @ApiProperty({ 49 | description: 'technology name like java', 50 | required: false, 51 | example: 'java', 52 | }) 53 | @IsString() 54 | @MinLength(2) 55 | public technology!: string; 56 | } 57 | 58 | export class UpdateQuestionBody extends PartialType(QuestionBodyDto) { } 59 | 60 | export class SearchParamDto { 61 | @ApiProperty({ 62 | description: 'search_term [name, description for search', 63 | required: false, 64 | }) 65 | @IsOptional() 66 | @IsString() 67 | @MinLength(2) 68 | public search_term!: string; 69 | 70 | @ApiProperty({ 71 | description: 'tags for search', 72 | type: String, 73 | example: 'tag', 74 | required: false, 75 | }) 76 | @IsOptional() 77 | @IsString() 78 | public tags?: string; 79 | 80 | @ApiProperty({ 81 | description: 'page number for request', 82 | required: false, 83 | }) 84 | @IsOptional() 85 | @Type(() => Number) 86 | @IsNumber() 87 | @Min(1) 88 | public page?: number; 89 | 90 | @ApiProperty({ 91 | description: 'number of records in a request', 92 | required: false, 93 | }) 94 | @IsOptional() 95 | @Type(() => Number) 96 | @IsNumber() 97 | @Min(1) 98 | public limit?: number; 99 | } 100 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/questions/question.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | CreateDateColumn, 5 | UpdateDateColumn, 6 | Entity, 7 | PrimaryGeneratedColumn, 8 | OneToMany, 9 | } from 'typeorm'; 10 | import { AnswerEntity } from '../answers/answers.entity'; 11 | 12 | @Entity('questions') 13 | export class QuestionEntity extends BaseEntity { 14 | @PrimaryGeneratedColumn('uuid') 15 | public id!: string; 16 | 17 | @Column({ type: 'varchar', select: true }) 18 | public questions_text!: string; 19 | 20 | @Column({ type: 'varchar', default: null }) 21 | public tags!: string; 22 | 23 | @Column({ type: 'varchar', default: null }) 24 | public technology!: string; 25 | 26 | @Column({ type: 'varchar', default: null }) 27 | public comment!: string; 28 | 29 | @Column({ type: 'varchar', default: null }) 30 | public url!: string; 31 | 32 | @Column({ type: 'varchar', default: null }) 33 | public image!: string; 34 | 35 | @Column({ type: 'varchar', default: null }) 36 | public user_id!: string; 37 | 38 | @Column({ type: 'jsonb', default: null }) 39 | public user_metadata!: any; 40 | 41 | @OneToMany(() => AnswerEntity, (event) => event.question) 42 | public answers!: AnswerEntity[]; 43 | 44 | @CreateDateColumn({ 45 | type: 'timestamptz', 46 | default: () => 'CURRENT_TIMESTAMP', 47 | select: true, 48 | }) 49 | public created_at!: Date; 50 | 51 | @UpdateDateColumn({ 52 | type: 'timestamptz', 53 | default: () => 'CURRENT_TIMESTAMP', 54 | select: true, 55 | }) 56 | public updated_at!: Date; 57 | 58 | @UpdateDateColumn({ 59 | type: 'timestamptz', 60 | default: () => 'CURRENT_TIMESTAMP', 61 | select: true, 62 | }) 63 | public deleted_at!: Date; 64 | } 65 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/questions/question.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { QuestionEntity } from './question.entity'; 4 | import { QuestionController } from './question.controller'; 5 | import { QuestionService } from './question.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([QuestionEntity])], 9 | controllers: [QuestionController], 10 | providers: [QuestionService], 11 | exports: [QuestionService], 12 | }) 13 | export class QuestionModule {} 14 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/app/domain/questions/question.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NotFoundException, 4 | UnauthorizedException, 5 | } from '@nestjs/common'; 6 | import { QuestionEntity } from './question.entity'; 7 | import { Like, Repository, Connection, QueryRunner } from 'typeorm'; 8 | import { InjectRepository } from '@nestjs/typeorm'; 9 | import { 10 | QuestionBodyDto, 11 | SearchParamDto, 12 | UpdateQuestionBody, 13 | } from './question.dto'; 14 | import { debug } from 'debug'; 15 | import { UserMetaData } from '@dev/auth'; 16 | import { QuestionByIdDto } from '../answers/answer.dto'; 17 | 18 | const verbose = debug('api:verbose:handler'); 19 | const error = debug('api:error:handler'); 20 | 21 | @Injectable() 22 | export class QuestionService { 23 | constructor( 24 | @InjectRepository(QuestionEntity) 25 | private questionRepo: Repository, 26 | ) { } 27 | 28 | async createQuestion(body: QuestionBodyDto, user: UserMetaData) { 29 | try { 30 | return await this.questionRepo.save({ 31 | ...body, 32 | user_id: user.uid, 33 | user_metadata: { 34 | email: user.email, 35 | picture: user.picture, 36 | name: user.name 37 | }, 38 | }); 39 | } catch (err) { 40 | throw err; 41 | } 42 | } 43 | 44 | async validateAuthorization(user: UserMetaData, param: QuestionByIdDto) { 45 | const { id } = param; 46 | const question = await this.questionRepo.findOne({ 47 | where: { 48 | id, 49 | }, 50 | }); 51 | if (!question) { 52 | throw new NotFoundException(); 53 | } 54 | if (question.user_id === user.uid) { 55 | return question; 56 | } 57 | throw new UnauthorizedException(); 58 | } 59 | 60 | async updateQuestion( 61 | body: UpdateQuestionBody, 62 | user: UserMetaData, 63 | param: QuestionByIdDto, 64 | ) { 65 | try { 66 | const question = await this.validateAuthorization(user, param); 67 | return await this.questionRepo.save({ 68 | ...question, 69 | ...body, 70 | }); 71 | } catch (err) { 72 | throw err; 73 | } 74 | } 75 | async deleteQuestion(user: UserMetaData, param: QuestionByIdDto) { 76 | try { 77 | await this.questionRepo.delete({ id: param.id }); 78 | } catch (err) { 79 | throw err; 80 | } 81 | } 82 | 83 | async getQuestionById(param: QuestionByIdDto) { 84 | try { 85 | return await this.questionRepo.findOne({ 86 | where: { id: param.id }, 87 | relations: ['answers'], 88 | }); 89 | } catch (err) { 90 | throw err; 91 | } 92 | } 93 | 94 | async getAllQuestions(queryInput: SearchParamDto) { 95 | try { 96 | const column = [ 97 | 'count(*) OVER() AS count', 98 | 'questions_text', 99 | 'tags', 100 | 'technology', 101 | 'url', 102 | 'image', 103 | 'user_metadata', 104 | 'user_id', 105 | 'comment', 106 | 'created_at', 107 | 'updated_at', 108 | 'id', 109 | ]; 110 | const { tags, search_term, page, limit } = queryInput; 111 | const skippedItems = (page - 1) * limit; 112 | verbose(queryInput); 113 | let query = `SELECT ${column.join( 114 | ',', 115 | )} FROM questions where questions_text is not null`; 116 | 117 | if (search_term) { 118 | const queryString = `( 119 | questions_text ILIKE '%${search_term}%' 120 | OR 121 | technology ILIKE '%${search_term}%' 122 | )`; 123 | query = `${query} AND ${queryString}`; 124 | } 125 | if (tags && tags.length > 0) { 126 | let queryString = ``; 127 | for (const tag of tags.split(',')) { 128 | if (!queryString) { 129 | queryString = `${queryString} tags ILIKE '%${tag}%'`; 130 | } else { 131 | queryString = `${queryString} OR tags ILIKE '%${tag}%'`; 132 | } 133 | } 134 | query = `${query} AND (${queryString})`; 135 | } 136 | query = `${query} ORDER BY questions_text ASC LIMIT ${limit} offset ${skippedItems}`; 137 | 138 | const questions = await this.questionRepo.query(query); 139 | const count = parseInt((questions[0] && questions[0].count) || 0); 140 | 141 | return { 142 | questions, 143 | totalCount: count, 144 | }; 145 | } catch (err) { 146 | throw err; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/docs/swagger.config.ts: -------------------------------------------------------------------------------- 1 | import { SwaggerConfig } from './swagger.interface'; 2 | 3 | /** 4 | * Configuration for the swagger UI (found at /api). 5 | * Change this to suit your app! 6 | */ 7 | export const SWAGGER_CONFIG: SwaggerConfig = { 8 | title: 'stack overflow service', 9 | description: ' api specs', 10 | version: '1.0', 11 | tags: [], 12 | }; 13 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/docs/swagger.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Specifies configuration for the swagger UI (found at /api). 3 | */ 4 | export interface SwaggerConfig { 5 | title: string; 6 | description: string; 7 | version: string; 8 | tags: string[]; 9 | } 10 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/docs/swagger.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 3 | import * as basicAuth from 'express-basic-auth'; 4 | import { ConfigService } from '@dev/config'; 5 | import { SWAGGER_CONFIG } from './swagger.config'; 6 | 7 | /** 8 | * Creates an OpenAPI document for an application, via swagger. 9 | * @param app the nestjs application 10 | * @returns the OpenAPI document 11 | */ 12 | const SWAGGER_ENVS = ['local', 'development', 'production']; 13 | 14 | export function createDocument(app: INestApplication) { 15 | const builder = new DocumentBuilder() 16 | .setTitle(SWAGGER_CONFIG.title) 17 | .addBearerAuth( 18 | { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, 19 | 'authorization', 20 | ) 21 | .setDescription(SWAGGER_CONFIG.description) 22 | .setVersion(SWAGGER_CONFIG.version); 23 | for (const tag of SWAGGER_CONFIG.tags) { 24 | builder.addTag(tag); 25 | } 26 | const options = builder.build(); 27 | const env = app.get(ConfigService).get().env; 28 | const { username, password }: any = app.get(ConfigService).get().swagger; 29 | if (SWAGGER_ENVS.includes(env)) { 30 | app.use( 31 | '/docs', 32 | basicAuth({ 33 | challenge: true, 34 | users: { 35 | [username]: password, 36 | }, 37 | }), 38 | ); 39 | const document = SwaggerModule.createDocument(app, options); 40 | SwaggerModule.setup('docs', app, document); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/src/main.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | import { NestFactory } from '@nestjs/core'; 4 | import { AppModule } from './app.module'; 5 | import { createDocument } from './docs/swagger'; 6 | import { HttpExceptionFilter } from './app/core/interceptors/http.exception'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule); 10 | createDocument(app); 11 | app.useGlobalFilters(new HttpExceptionFilter()); 12 | 13 | await app.listen(3001); 14 | } 15 | bootstrap(); 16 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/test/setEnvVars.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | dotenv.config({ path: './env.test' }); 3 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./" 5 | }, 6 | "exclude": ["node_modules", "build"] 7 | } 8 | -------------------------------------------------------------------------------- /apps/stack-overflow-apis/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_PUBLIC_FIREBASE_KEY=XXXXXXXXXXXX 2 | REACT_APP_PUBLIC_AUTH_DOMAIN=swiggy-clone-50ab6.firebaseapp.com 3 | REACT_APP_PUBLIC_DATABASE_URL=https://swiggy-clone-50ab6.firebaseapp.com 4 | REACT_APP_PUBLIC_PROJECTID=swiggy-clone-50ab6 5 | REACT_APP_PUBLIC_STORAGE_BUCKET=swiggy-clone-50ab6.appspot.com 6 | REACT_APP_PUBLIC_SENDER_ID=XXXXXXXX 7 | REACT_APP_PUBLIC_APPID=1:538697283380:web:XXXXXXXXXXXXXX 8 | REACT_APP_PUBLIC_MEASUREMENT_ID=G-N52BL08Z00 9 | REACT_APP_PUBLIC_API_URL=http://localhost -------------------------------------------------------------------------------- /apps/stack-overflow-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .env 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev/stack-overflow-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:3001", 6 | "dependencies": { 7 | "@material-ui/core": "^4.12.3", 8 | "@material-ui/icons": "^4.11.2", 9 | "@reduxjs/toolkit": "^1.7.1", 10 | "@testing-library/jest-dom": "^5.16.5", 11 | "@testing-library/react": "^13.4.0", 12 | "@testing-library/user-event": "^13.5.0", 13 | "@types/jest": "^27.5.2", 14 | "@types/node": "^16.18.23", 15 | "@types/react": "^18.0.35", 16 | "@types/react-dom": "^18.0.11", 17 | "@types/react-redux": "^7.1.25", 18 | "@types/react-router-dom": "^5.3.3", 19 | "axios": "^1.3.5", 20 | "firebase": "^9.6.1", 21 | "html-react-parser": "^1.4.2", 22 | "javascript-time-ago": "^2.3.10", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-quill": "^1.3.5", 26 | "react-redux": "^7.2.6", 27 | "react-responsive-modal": "^6.1.0", 28 | "react-router-dom": "^6.10.0", 29 | "react-scripts": "5.0.1", 30 | "react-time-ago": "^7.1.4", 31 | "typescript": "^4.9.5", 32 | "web-vitals": "^2.1.4" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject" 39 | }, 40 | "eslintConfig": { 41 | "extends": [ 42 | "react-app", 43 | "react-app/jest" 44 | ] 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkssharma/stack-overflow-full-stack-clone/5fbfa5cf64413e23b83182793c590aabd94d9fbd/apps/stack-overflow-ui/public/favicon.ico -------------------------------------------------------------------------------- /apps/stack-overflow-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkssharma/stack-overflow-full-stack-clone/5fbfa5cf64413e23b83182793c590aabd94d9fbd/apps/stack-overflow-ui/public/logo192.png -------------------------------------------------------------------------------- /apps/stack-overflow-ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkssharma/stack-overflow-full-stack-clone/5fbfa5cf64413e23b83182793c590aabd94d9fbd/apps/stack-overflow-ui/public/logo512.png -------------------------------------------------------------------------------- /apps/stack-overflow-ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Roboto", sans-serif; 4 | } 5 | 6 | * { 7 | box-sizing: border-box; 8 | scroll-behavior: smooth; 9 | } 10 | 11 | .btn:hover { 12 | box-shadow: unset !important; 13 | } 14 | 15 | input[type="button"] { 16 | outline: none !important; 17 | } 18 | 19 | button::selection { 20 | outline: none !important; 21 | } 22 | 23 | .btn::selection { 24 | outline: none; 25 | } 26 | 27 | button:focus { 28 | outline: none !important; 29 | } 30 | 31 | :root { 32 | --vp: 0.052vw; 33 | --vm: 0.278vw; 34 | } 35 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { BrowserRouter, Routes, Route, Navigate, Link, useNavigate } from "react-router-dom"; 4 | import Questions from "./components/questions/overflow"; 5 | 6 | import Login from "./components/auth/login"; 7 | import { auth } from "./firebase"; 8 | import { onAuthStateChanged } from "firebase/auth"; 9 | import { login } from "./features/user.slice"; 10 | 11 | function App() { 12 | const dispatch = useDispatch(); 13 | const navigate = useNavigate(); 14 | 15 | useEffect(() => { 16 | onAuthStateChanged(auth, async (authUser) => { 17 | if(authUser) { 18 | dispatch(login({ 19 | userName: authUser.displayName, 20 | access_token: await authUser.getIdToken(), 21 | photo: authUser.photoURL, 22 | email: authUser.email, 23 | uid: authUser.uid 24 | })) 25 | } 26 | navigate("/") 27 | }) 28 | }, [dispatch]) 29 | 30 | return ( 31 |
32 | 33 | } /> 34 | } /> 35 | } /> 36 | 37 |
38 | ); 39 | } 40 | 41 | export default App; 42 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/auth/login.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .login-container { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | height: 100vh; 8 | width: 100vw; 9 | box-sizing: border-box; 10 | } 11 | 12 | .login-content { 13 | display: flex; 14 | align-items: center; 15 | flex-direction: column; 16 | } 17 | 18 | .login-content > img { 19 | width: 200px; 20 | margin: 20px; 21 | object-fit: contain; 22 | } 23 | 24 | .login-content > button { 25 | padding: 10px; 26 | margin: 50px 0px; 27 | background-color: #000; 28 | color: #fff; 29 | outline: none; 30 | border: none; 31 | border-radius: 3px;; 32 | font-family: monospace; 33 | cursor: pointer; 34 | letter-spacing: 1.1px; 35 | } -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/auth/login.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { signInWithPopup } from "firebase/auth"; 3 | import { auth, provider } from "../../firebase"; 4 | import Logo from "../img/logo.png"; 5 | import "./login.css" 6 | function Login() { 7 | 8 | const handleClick = async () => { 9 | await signInWithPopup(auth, provider) 10 | .then((result) => { 11 | console.log(result) 12 | }).catch((error) => { 13 | console.log(error); 14 | }); 15 | } 16 | 17 | return ( 18 |
19 |
20 | logo 21 | 22 |
23 |
24 | 25 | ) 26 | } 27 | 28 | export default Login; -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkssharma/stack-overflow-full-stack-clone/5fbfa5cf64413e23b83182793c590aabd94d9fbd/apps/stack-overflow-ui/src/components/img/logo.png -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/question-with-answers/overflow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { logout, selectUser } from "../../features/user.slice"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { auth } from "../../firebase"; 5 | import { signOut } from "firebase/auth"; 6 | import { BrowserRouter, Routes, Route, Navigate, Link, useNavigate } from "react-router-dom"; 7 | import { fetchQuestions, fetchQuestionsById } from "../../features/question.slice"; 8 | 9 | 10 | function Overflow(){ 11 | 12 | const dispatch = useDispatch(); 13 | const navigate = useNavigate(); 14 | 15 | useEffect(() => { 16 | dispatch(fetchQuestions()) 17 | dispatch(fetchQuestionsById('a2cf4c60-0605-485c-aeb8-b84171771cca')) 18 | 19 | }, [dispatch]) 20 | 21 | const handleLogout = () => { 22 | signOut(auth) 23 | .then(() => { 24 | dispatch(logout()); 25 | navigate("/login"); 26 | console.log("Logged out"); 27 | }) 28 | .catch(() => { 29 | console.log("error in logout"); 30 | }); 31 | 32 | }; 33 | const user = useSelector(selectUser); 34 | 35 | return ( 36 |
37 |

{JSON.stringify(user)}

38 | 39 |
40 | ) 41 | } 42 | 43 | export default Overflow; -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/css/Feed.css: -------------------------------------------------------------------------------- 1 | .feed { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 0.6; 5 | } 6 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/css/Overflow.css: -------------------------------------------------------------------------------- 1 | .overflow { 2 | width: "100%"; 3 | min-width: fit-content; 4 | } 5 | 6 | .overflow__contents { 7 | display: flex; 8 | justify-content: center; 9 | padding: 50px 0; 10 | background-color: rgba(0, 0, 0, 0.05); 11 | min-width: fit-content; 12 | width: 100%; 13 | } 14 | 15 | .overflow__content { 16 | display: flex; 17 | flex-direction: row; 18 | padding: 0px 10px; 19 | width: 100%; 20 | max-width: 1200px; 21 | } 22 | 23 | .react-responsive-modal-modal { 24 | width: 90vw; 25 | height: 90vh; 26 | } 27 | 28 | .quill { 29 | height: 50vh; 30 | } 31 | 32 | .react-responsive-modal-overlay { 33 | background: rgb(0 0 0 / 75%); 34 | } 35 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/css/OverflowBox.css: -------------------------------------------------------------------------------- 1 | .overflowBox { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 10px; 5 | border: 1px solid lightgray; 6 | background-color: white; 7 | border-radius: 5px; 8 | cursor: pointer; 9 | max-width: 700px; 10 | } 11 | 12 | .overflowBox:hover { 13 | border: 1px solid rgb(175, 175, 175); 14 | } 15 | 16 | .overflowBox__info { 17 | display: flex; 18 | align-items: center; 19 | } 20 | 21 | .overflowBox__info > h5 { 22 | color: rgb(129, 129, 129); 23 | font-weight: bold; 24 | margin-left: 10px; 25 | font-size: large; 26 | } 27 | 28 | .overflowBox__overflow { 29 | display: flex; 30 | margin-top: 8px; 31 | } 32 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/css/OverflowHeader.css: -------------------------------------------------------------------------------- 1 | .qHeader { 2 | display: flex; 3 | align-items: center; 4 | background-color: #fff; 5 | position: sticky; 6 | z-index: 1000; 7 | top: 0px; 8 | /* box-shadow: 0px 5px 8px -9px rgba(0, 0, 0, 0.5); */ 9 | box-shadow: 2px 3px 6px rgba(187, 187, 187, 0.5); 10 | justify-content: center; 11 | padding: 3px; 12 | } 13 | 14 | .qHeader-content { 15 | display: flex; 16 | align-items: center; 17 | justify-content: space-between; 18 | /* padding: 5px; */ 19 | } 20 | 21 | .qHeader__logo > img { 22 | height: 30px !important; 23 | object-fit: contain; 24 | } 25 | 26 | .qHeader__icons { 27 | display: flex; 28 | } 29 | 30 | .qHeader__icon { 31 | padding: 5px; 32 | cursor: pointer; 33 | } 34 | 35 | .qHeader__icon:hover { 36 | background-color: #eee; 37 | border-radius: 5px; 38 | } 39 | 40 | .qHeader__icon > .MuiSvgIcon-root { 41 | color: gray; 42 | font-size: xx-large; 43 | margin-left: 20px; 44 | margin-right: 20px; 45 | } 46 | 47 | .qHeader__icon:hover > .MuiSvgIcon-root { 48 | color: #000; 49 | } 50 | 51 | .qHeader__input { 52 | display: flex; 53 | align-items: center; 54 | border: 1px solid lightgray; 55 | padding: 5px; 56 | border-radius: 5px; 57 | margin-left: 5px; 58 | } 59 | 60 | .qHeader__input > input { 61 | background-color: transparent; 62 | outline: none; 63 | border: none; 64 | color: rgb(49, 49, 49); 65 | } 66 | 67 | .qHeader__input > .MuiSvgIcon-root { 68 | color: gray; 69 | } 70 | 71 | .qHeader__avatar { 72 | cursor: pointer; 73 | } 74 | .qHeader__Rem { 75 | display: flex; 76 | align-items: center; 77 | margin-left: 25px; 78 | } 79 | 80 | .qHeader__Rem > .MuiSvgIcon-root { 81 | font-size: xx-large; 82 | color: gray; 83 | margin-left: 25px; 84 | cursor: pointer; 85 | } 86 | .qHeader__Rem > .MuiSvgIcon-root:hover { 87 | color: #000; 88 | } 89 | 90 | .qHeader__Rem > .MuiButton-root { 91 | color: white; 92 | background: #222; 93 | text-transform: inherit; 94 | border-radius: 5px; 95 | margin-left: 25px; 96 | } 97 | 98 | .qHeader__Rem > .MuiButton-root:hover { 99 | color: #222; 100 | background: rgb(214, 214, 214); 101 | } 102 | 103 | .modal__title { 104 | display: flex; 105 | align-items: center; 106 | margin-bottom: 5px; 107 | border-bottom: 1px solid rgba(187, 187, 187, 0.5); 108 | } 109 | 110 | .modal__title > h5 { 111 | color: gray; 112 | font-size: 20px; 113 | cursor: pointer; 114 | font-weight: 500; 115 | margin-right: 30px; 116 | } 117 | 118 | .modal__title > h5:hover { 119 | color: #b92b27; 120 | } 121 | 122 | .modal__info { 123 | display: flex; 124 | align-items: center; 125 | margin-top: 30px; 126 | } 127 | 128 | .modal__info > p { 129 | margin-left: 10px; 130 | font-size: small; 131 | color: gray; 132 | } 133 | 134 | .modal__info > .modal__scope { 135 | display: flex; 136 | align-items: center; 137 | color: rgb(98, 98, 98); 138 | padding: 5px; 139 | margin-left: 10px; 140 | background-color: rgb(230, 230, 230); 141 | border-radius: 33px; 142 | cursor: pointer; 143 | } 144 | 145 | .modal__Field { 146 | display: flex; 147 | flex-direction: column; 148 | margin-top: 30px; 149 | flex: 1; 150 | } 151 | 152 | .modal__Field > .modal__fieldLink { 153 | color: gray; 154 | display: flex; 155 | margin-top: 10px; 156 | align-items: center; 157 | } 158 | 159 | .modal__Field > .modal__fieldLink > input { 160 | flex: 1; 161 | border: none; 162 | outline: none; 163 | margin-left: 5px; 164 | } 165 | 166 | .modal__buttons { 167 | display: flex; 168 | flex-direction: column-reverse; 169 | margin-top: 10px; 170 | align-items: center; 171 | } 172 | 173 | .modal__buttons > .cancle { 174 | margin-top: 10px; 175 | border: none; 176 | outline: none; 177 | color: gray; 178 | font-weight: 500; 179 | padding: 10px; 180 | border-radius: 33px; 181 | cursor: pointer; 182 | } 183 | 184 | .modal__buttons > .cancle:hover { 185 | color: red; 186 | } 187 | 188 | .modal__buttons > .add { 189 | border: none; 190 | outline: none; 191 | margin-top: 10px; 192 | background-color: #222; 193 | color: white; 194 | font-weight: 700; 195 | padding: 10px; 196 | border-radius: 33px; 197 | cursor: pointer; 198 | width: 50%; 199 | } 200 | 201 | .modal__buttons > .add:hover { 202 | background-color: #eee; 203 | color: #222; 204 | } 205 | 206 | @media only screen and (max-width: 600px) { 207 | .qHeader__icons { 208 | display: none; 209 | } 210 | 211 | .qHeader__input { 212 | display: none; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/css/Post.css: -------------------------------------------------------------------------------- 1 | .post { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 10px; 5 | background-color: white; 6 | margin-top: 10px; 7 | border: 1px solid lightgray; 8 | border-radius: 5px; 9 | max-width: 700px; 10 | box-shadow: 0px 5px 8px -9px solid rgab(0, 0, 0, 0.5); 11 | } 12 | 13 | .post__info { 14 | display: flex; 15 | align-items: center; 16 | } 17 | 18 | .post__info > h4 { 19 | margin-left: 10px; 20 | cursor: pointer; 21 | font-size: 13px; 22 | } 23 | .post__info > h4:hover { 24 | text-decoration: underline; 25 | } 26 | 27 | .post__info > small { 28 | margin-left: 10px; 29 | } 30 | 31 | .post__body { 32 | display: flex; 33 | flex-direction: column; 34 | } 35 | 36 | .post__body > .post__question { 37 | margin-top: 10px; 38 | font-weight: bold; 39 | margin-bottom: 10px; 40 | cursor: pointer; 41 | display: flex; 42 | align-items: center; 43 | flex: 1; 44 | } 45 | 46 | .post__question > .post__btnAnswer { 47 | margin-left: auto; 48 | cursor: pointer; 49 | padding: 7px; 50 | background-color: #222; 51 | outline: none; 52 | border: none; 53 | color: lightgray; 54 | font-weight: 300; 55 | font-size: 14px; 56 | border-radius: 5px; 57 | } 58 | 59 | .post__btnAnswer:hover { 60 | color: #222; 61 | background: lightgray; 62 | } 63 | 64 | .post__question > p:hover { 65 | text-decoration: underline; 66 | } 67 | 68 | .post__body > .post__answer > p { 69 | margin-bottom: 10px; 70 | } 71 | 72 | /* .post__image { 73 | background: rgb(0, 140, 255); 74 | } */ 75 | 76 | .post__body > img { 77 | width: 100%; 78 | max-height: 400px; 79 | object-fit: contain; 80 | background-color: transparent; 81 | border-radius: 5px; 82 | cursor: pointer; 83 | margin-top: 10px; 84 | } 85 | 86 | .post__footer { 87 | display: flex; 88 | align-items: center; 89 | margin-top: 5px; 90 | } 91 | 92 | .post__footer > .MuiSvgIcon-root { 93 | color: gray; 94 | margin-right: 40px; 95 | cursor: pointer; 96 | } 97 | 98 | .post__footerAction { 99 | background-color: lightgray; 100 | padding: 5px; 101 | align-items: center; 102 | display: flex; 103 | justify-content: space-around; 104 | border-radius: 33px; 105 | } 106 | .post__footerAction > .MuiSvgIcon-root { 107 | color: gray; 108 | margin-right: 40px; 109 | cursor: pointer; 110 | } 111 | 112 | .post__footerAction > .MuiSvgIcon-root:hover { 113 | color: rgb(0, 140, 255); 114 | margin-right: 40px; 115 | } 116 | 117 | .post__footerLeft { 118 | margin-left: auto; 119 | } 120 | 121 | .post__footerLeft > .MuiSvgIcon-root { 122 | color: gray; 123 | cursor: pointer; 124 | margin-left: 30px; 125 | } 126 | 127 | .modal__question { 128 | display: flex; 129 | align-items: center; 130 | flex-direction: column; 131 | margin-top: 20px; 132 | } 133 | 134 | .modal__question > h1 { 135 | color: #8f1f1b; 136 | font-weight: 600; 137 | margin-bottom: 10px; 138 | } 139 | 140 | .modal__question > p { 141 | color: gray; 142 | font-size: small; 143 | } 144 | 145 | .modal__question > p > .name { 146 | color: black; 147 | font-weight: bold; 148 | } 149 | 150 | .modal__answer { 151 | display: flex; 152 | padding-top: 20px; 153 | flex: 1; 154 | } 155 | 156 | .modal__answer > textarea { 157 | width: 100%; 158 | height: 200px; 159 | padding: 5px; 160 | font-size: 15px; 161 | color: black; 162 | } 163 | 164 | .modal__button { 165 | display: flex; 166 | align-items: center; 167 | flex-direction: row; 168 | justify-content: space-between; 169 | margin-top: 50px; 170 | width: 100%; 171 | } 172 | 173 | .modal__button > .cancle { 174 | border: none; 175 | margin-top: 10px; 176 | outline: none; 177 | color: gray; 178 | font-weight: 500; 179 | padding: 10px; 180 | border-radius: 33px; 181 | cursor: pointer; 182 | } 183 | 184 | .modal__button > .cancle:hover { 185 | color: red; 186 | } 187 | .modal__button > .add { 188 | border: none; 189 | outline: none; 190 | margin-top: 5px; 191 | background-color: #222; 192 | color: white; 193 | font-weight: 700; 194 | padding: 10px; 195 | border-radius: 33px; 196 | cursor: pointer; 197 | width: 50%; 198 | } 199 | .modal__button > .add:hover { 200 | background-color: #eee; 201 | color: #222; 202 | } 203 | 204 | .react-responsive-modal-modal { 205 | /* width: 600px; */ 206 | height: 400px; 207 | } 208 | 209 | .quill { 210 | width: 100%; 211 | height: 120px; 212 | } 213 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/css/Sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | /* margin-top: 10px; */ 3 | margin-right: 10px; 4 | flex: 0.2; 5 | } 6 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/css/SidebarOptions.css: -------------------------------------------------------------------------------- 1 | .sidebarOptions { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | .sidebarOption { 6 | display: flex; 7 | align-items: center; 8 | padding: 10px; 9 | cursor: pointer; 10 | } 11 | 12 | .sidebarOption:hover { 13 | background: rgb(211, 211, 211); 14 | border-radius: 5px; 15 | } 16 | .sidebarOption > img { 17 | height: 30px; 18 | border-radius: 5px; 19 | margin-left: 20px; 20 | } 21 | 22 | .sidebarOption > .MuiSvgIcon-root { 23 | font-size: medium; 24 | margin-left: 20px; 25 | } 26 | .sidebarOption > p { 27 | margin-left: 10px; 28 | color: gray; 29 | font-weight: 400; 30 | font-size: 13px; 31 | } 32 | 33 | .sidebarOption > .text { 34 | margin-left: 23px; 35 | color: gray; 36 | font-weight: 400; 37 | font-size: 13px; 38 | } 39 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/css/Widget.css: -------------------------------------------------------------------------------- 1 | .widget { 2 | flex: 0.2; 3 | margin-left: 20px; 4 | display: flex; 5 | flex-direction: column; 6 | margin-right: 20px; 7 | border: 1px solid lightgray; 8 | border-radius: 5px; 9 | height: fit-content; 10 | background-color: white; 11 | } 12 | 13 | .widget__header { 14 | border-bottom: 1px solid lightgray; 15 | } 16 | 17 | .widget__header > h5 { 18 | padding: 10px; 19 | color: rgb(80, 80, 80); 20 | font-size: 15px; 21 | letter-spacing: 1.1px; 22 | } 23 | 24 | .widget__contents { 25 | display: flex; 26 | flex-direction: column; 27 | } 28 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/css/WidgetContent.css: -------------------------------------------------------------------------------- 1 | .widget__contents { 2 | display: flex; 3 | cursor: pointer; 4 | } 5 | 6 | .widget__content { 7 | margin: 5px; 8 | display: flex; 9 | padding: 5px; 10 | border-bottom: 1px solid lightgray; 11 | } 12 | 13 | .widget__content > img { 14 | height: 30px; 15 | border-radius: 5px; 16 | } 17 | 18 | .widget__contentTitle { 19 | margin-left: 10px; 20 | } 21 | .widget__contentTitle > h5 { 22 | color: rgb(70, 68, 68); 23 | } 24 | 25 | .widget__contentTitle > p { 26 | color: gray; 27 | font-size: small; 28 | } 29 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/overflow-box.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Input } from "@material-ui/core"; 2 | import React, { useState } from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import "./css/OverflowBox.css"; 5 | import { selectUser } from "../../features/user.slice"; 6 | import { PeopleAltOutlined, ExpandMore } from "@material-ui/icons"; 7 | import { useNavigate } from "react-router-dom"; 8 | import CloseIcon from "@material-ui/icons/Close"; 9 | import { Modal } from "react-responsive-modal"; 10 | import { createQuestion } from "../../features/question.slice"; 11 | 12 | function QuoraBox() { 13 | 14 | const [isModalOpen, setIsModalOpen] = useState(false); 15 | const [inputUrl, setInputUrl] = useState(""); 16 | const [question, setQuestion] = useState(""); 17 | const Close = ; 18 | const dispatch = useDispatch(); 19 | const user = useSelector(selectUser); 20 | const navigate = useNavigate(); 21 | 22 | const handleChange = (e: any) => { 23 | console.log(e.target.value); 24 | setQuestion(e.target.value); 25 | } 26 | 27 | const handleSubmit = () => { 28 | alert(question); 29 | dispatch(createQuestion({ 30 | "questions_text": question, 31 | "tags": "lambda, aws, java, nestjs, auth0", 32 | "url": "https://stackoverflow.com/questions/50493011/react-ui-router-test-state-change", 33 | "image": "https://cdn-media-1.freecodecamp.org/images/1*TKvlTeNqtkp1s-eVB5Hrvg@2x.png", 34 | "technology": "javascript" 35 | })); 36 | }; 37 | 38 | return ( 39 |
40 |
setIsModalOpen(true)} className="overflowBox__info"> 41 | 42 |
43 |
44 |
setIsModalOpen(true)}>What is your question or link?
45 |
46 | setIsModalOpen(false)} 50 | closeOnEsc 51 | center 52 | closeOnOverlayClick={false} 53 | styles={{ 54 | overlay: { 55 | height: "auto", 56 | }, 57 | }} 58 | > 59 |
60 |
Add Question
61 |
Share Link
62 |
63 |
64 | 65 |
66 | 67 |

Public

68 | 69 |
70 |
71 |
72 | 84 |
90 | setInputUrl(e.target.value)} 94 | style={{ 95 | margin: "5px 0", 96 | border: "1px solid lightgray", 97 | padding: "10px", 98 | outline: "2px solid #000", 99 | }} 100 | placeholder="Optional: inclue a link that gives context" 101 | /> 102 | {inputUrl !== "" && ( 103 | displayimage 111 | )} 112 |
113 |
114 |
115 | 118 | 121 |
122 |
123 |
124 | ); 125 | } 126 | 127 | export default QuoraBox; 128 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/overflow-header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import HomeIcon from "@material-ui/icons/Home"; 3 | import FeaturedPlayListOutlinedIcon from "@material-ui/icons/FeaturedPlayListOutlined"; 4 | import { 5 | AssignmentTurnedInOutlined, 6 | // Close, 7 | NotificationsOutlined, 8 | PeopleAltOutlined, 9 | Search, 10 | ExpandMore, 11 | } from "@material-ui/icons"; 12 | import CloseIcon from "@material-ui/icons/Close"; 13 | import { Avatar, Button, Input } from "@material-ui/core"; 14 | import "./css/OverflowHeader.css"; 15 | import { Modal } from "react-responsive-modal"; 16 | import "react-responsive-modal/styles.css"; 17 | import axios from "axios"; 18 | import { auth } from "../../firebase"; 19 | import { signOut } from "firebase/auth"; 20 | import { useDispatch, useSelector } from "react-redux"; 21 | import Logo from "../img/logo.png"; 22 | import { BrowserRouter, Routes, Route, Navigate, Link, useNavigate } from "react-router-dom"; 23 | import { logout, selectUser } from "../../features/user.slice"; 24 | 25 | function QuoraHeader() { 26 | const [isModalOpen, setIsModalOpen] = useState(false); 27 | const [inputUrl, setInputUrl] = useState(""); 28 | const [question, setQuestion] = useState(""); 29 | const Close = ; 30 | const dispatch = useDispatch(); 31 | const user = useSelector(selectUser); 32 | const navigate = useNavigate(); 33 | 34 | const handleSubmit = () => { 35 | }; 36 | 37 | const handleLogout = () => { 38 | signOut(auth) 39 | .then(() => { 40 | dispatch(logout()); 41 | navigate("/login"); 42 | }) 43 | .catch(() => { 44 | console.log("error in logout"); 45 | }); 46 | 47 | }; 48 | return ( 49 |
50 |
51 |
52 | logo 56 |
57 |
58 |
59 | 60 |
61 |
62 | 63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 |
71 | 72 |
73 |
74 |
75 | 76 | 77 |
78 |
79 | 80 | 81 | 82 | 83 | 84 | setIsModalOpen(false)} 88 | closeOnEsc 89 | center 90 | closeOnOverlayClick={false} 91 | styles={{ 92 | overlay: { 93 | height: "auto", 94 | }, 95 | }} 96 | > 97 |
98 |
Add Question
99 |
Share Link
100 |
101 |
102 | 103 |
104 | 105 |

Public

106 | 107 |
108 |
109 |
110 | setQuestion(e.target.value)} 113 | type=" text" 114 | placeholder="Start your question with 'What', 'How', 'Why', etc. " 115 | /> 116 |
122 | setInputUrl(e.target.value)} 126 | style={{ 127 | margin: "5px 0", 128 | border: "1px solid lightgray", 129 | padding: "10px", 130 | outline: "2px solid #000", 131 | }} 132 | placeholder="Optional: inclue a link that gives context" 133 | /> 134 | {inputUrl !== "" && ( 135 | displayimage 143 | )} 144 |
145 |
146 |
147 | 150 | 153 |
154 |
155 |
156 |
157 |
158 | ); 159 | } 160 | 161 | export default QuoraHeader; 162 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/overflow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { logout, selectUser } from "../../features/user.slice"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { auth } from "../../firebase"; 5 | import { signOut } from "firebase/auth"; 6 | import { BrowserRouter, Routes, Route, Navigate, Link, useNavigate, useParams } from "react-router-dom"; 7 | import { fetchQuestions, fetchQuestionsById } from "../../features/question.slice"; 8 | import OverflowHeader from "./overflow-header"; 9 | import Sidebar from "./sidebar"; 10 | import QuestionFeed from "./questions-feed"; 11 | import Widget from "./widget"; 12 | import "./css/Overflow.css"; 13 | import QuestionAnswerFeed from "./questions-answer-feed"; 14 | 15 | 16 | function Overflow(){ 17 | 18 | const { id } = useParams(); 19 | const dispatch = useDispatch(); 20 | const navigate = useNavigate(); 21 | 22 | return ( 23 |
24 | 25 |
26 |
27 | 28 | {!id ? : } 29 | 30 |
31 |
32 |
33 | ) 34 | } 35 | 36 | export default Overflow; -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/question.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar } from "@material-ui/core"; 2 | import { 3 | ArrowDownwardOutlined, 4 | ArrowUpwardOutlined, 5 | ChatBubbleOutlined, 6 | MoreHorizOutlined, 7 | RepeatOneOutlined, 8 | ShareOutlined, 9 | } from "@material-ui/icons"; 10 | import React, { useState } from "react"; 11 | import "./css/Post.css"; 12 | import { Modal } from "react-responsive-modal"; 13 | import "react-responsive-modal/styles.css"; 14 | import CloseIcon from "@material-ui/icons/Close"; 15 | import ReactQuill from "react-quill"; 16 | import "react-quill/dist/quill.snow.css"; 17 | import ReactTimeAgo from "react-time-ago"; 18 | import axios from "axios"; 19 | import ReactHtmlParser from "html-react-parser"; 20 | import { useDispatch, useSelector } from "react-redux"; 21 | import { selectUser } from "../../features/user.slice"; 22 | import { useNavigate } from "react-router-dom"; 23 | import { updateAnswersVotes } from "../../features/answer.slice"; 24 | 25 | function LastSeen({ date }: any) { 26 | return ( 27 |
28 | 29 |
30 | ); 31 | } 32 | function Post({ post }: any) { 33 | const [isModalOpen, setIsModalOpen] = useState(false); 34 | const [answer, setAnswer] = useState(""); 35 | const Close = ; 36 | const dispatch = useDispatch(); 37 | 38 | const user = useSelector(selectUser); 39 | 40 | const navigate = useNavigate(); 41 | const handleQuill = (value: any) => { 42 | setAnswer(value); 43 | }; 44 | 45 | const handleUpVote = (questionId: string , answerId: string) => { 46 | dispatch(updateAnswersVotes({questionId, answerId, vote: true})) 47 | } 48 | const handleDownVote = (questionId: string , answerId: string) => { 49 | dispatch(updateAnswersVotes({questionId, answerId, vote: false})) 50 | } 51 | 52 | const handleSubmit = async () => { 53 | 54 | }; 55 | return ( 56 |
57 |
58 | 59 |

{post?.user_metadata?.email}

60 |
{post?.user_metadata?.name}
61 | 62 | 63 |
64 |
65 |
66 |

{post?.questions_text}

67 | 75 | setIsModalOpen(false)} 79 | closeOnEsc 80 | center 81 | closeOnOverlayClick={false} 82 | styles={{ 83 | overlay: { 84 | height: "auto", 85 | }, 86 | }} 87 | > 88 |
89 |

{post?.question_text}

90 |

91 | asked by {post?.user_metadata?.email} on{" "} 92 | 93 | {new Date(post?.created_at).toLocaleString()} 94 | 95 |

96 |
97 |
98 | 103 |
104 |
105 | 108 | 111 |
112 |
113 |
114 | {post.image !== "" && { 116 | navigate(`/questions/${post.id}`); 117 | }} 118 | src={post.image} alt="url" />} 119 |
120 |

128 | {post?.answers?.length} Answer(s) 129 |

130 | 131 |
139 | {post?.answers?.map((_a: any) => ( 140 | <> 141 |
151 |
162 | 163 |
169 |

{_a?.user_metadata?.name}

170 | 171 | {_a?.created_at} 172 | 173 |
174 |
175 |
{ReactHtmlParser(_a?.answer_text)}
176 |
177 |
178 | {_a?.upvote} 179 | handleUpVote(post.id, _a.id)} /> 180 | {_a?.downvote} 181 | handleDownVote(post.id, _a.id)} /> 182 |
183 | 184 | 185 |
186 | 187 | 188 |
189 |
190 |
191 | 192 | ))} 193 |
194 |
195 | ); 196 | } 197 | 198 | export default Post; 199 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/questions-answer-feed.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import QuoraBox from "./overflow-box"; 3 | import "./css/Feed.css"; 4 | 5 | import Question from "./question"; 6 | import axios from "axios"; 7 | import { fetchQuestions, fetchQuestionsById, questionsSelector, selectedQuestionSelector } from "../../features/question.slice"; 8 | import { useDispatch, useSelector } from "react-redux"; 9 | import { fetchQuestionsAnswers } from "../../features/answer.slice"; 10 | 11 | function QuestionAnswerFeed({id }: any) { 12 | const dispatch = useDispatch(); 13 | const question = useSelector(selectedQuestionSelector); 14 | 15 | useEffect(() => { 16 | dispatch(fetchQuestionsById(id)) 17 | }, [dispatch]); 18 | return ( 19 |
20 | 21 | 22 |
23 | ); 24 | } 25 | 26 | export default QuestionAnswerFeed; 27 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/questions-feed.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import QuoraBox from "./overflow-box"; 3 | import "./css/Feed.css"; 4 | import Question from "./question"; 5 | import axios from "axios"; 6 | import { fetchQuestions, questionsSelector } from "../../features/question.slice"; 7 | import { useDispatch, useSelector } from "react-redux"; 8 | 9 | function QuestionFeed() { 10 | const dispatch = useDispatch(); 11 | const questions = useSelector(questionsSelector); 12 | 13 | useEffect(() => { 14 | dispatch(fetchQuestions()) 15 | }, [dispatch]); 16 | return ( 17 |
18 | 19 | {questions.map((question: any, index: number) => ( 20 | 21 | ))} 22 |
23 | ); 24 | } 25 | 26 | export default QuestionFeed; 27 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/side-bar-options.tsx: -------------------------------------------------------------------------------- 1 | import { Add } from "@material-ui/icons"; 2 | import React from "react"; 3 | import "./css/SidebarOptions.css"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { fetchQuestions, questionsSelector } from "../../features/question.slice"; 6 | 7 | function SidebarOptions() { 8 | const questions = useSelector(questionsSelector); 9 | const dispatch = useDispatch(); 10 | // ["nodejs, react", "nodejs, web"] 11 | const appTags: string[] = []; 12 | const tags = questions.map((i: any) => i.tags) 13 | .forEach((i: string) => { 14 | const questionTags = i.split(','); 15 | for (const tag of questionTags) { 16 | if (!appTags.includes(tag)) { 17 | appTags.push(tag); 18 | } 19 | } 20 | }) 21 | const handleClick = (tags: string) => { 22 | dispatch(fetchQuestions(tags)) 23 | } 24 | 25 | return ( 26 |
27 | {appTags && appTags.length > 0 && 28 | ( 29 | appTags.map(i => { 30 | return ( 31 |
handleClick(i)} className="sidebarOption"> 32 | 36 |

{i}

37 |
38 | ) 39 | }) 40 | )} 41 |
42 | 43 |

Discover Spaces

44 |
45 |
46 | ); 47 | } 48 | 49 | export default SidebarOptions; 50 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./css/Sidebar.css"; 3 | import SidebarOptions from "./side-bar-options"; 4 | 5 | function Sidebar() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | 13 | export default Sidebar; 14 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/widget-content.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./css/WidgetContent.css"; 3 | 4 | function WidgetContent() { 5 | return ( 6 |
7 |
8 | 12 |
13 |
Mobile App Programmer
14 |

The best Mobile App Development Company

15 |
16 |
17 |
18 | ); 19 | } 20 | 21 | export default WidgetContent; 22 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/components/questions/widget.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import WidgetContent from "./widget-content"; 3 | import "./css/Widget.css"; 4 | 5 | function Widget() { 6 | return ( 7 |
8 |
9 |
Space to follow
10 |
11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export default Widget; 19 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/features/answer.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; 2 | import axios from "axios"; 3 | 4 | export interface Answer { 5 | status: "idle" | "pending" | "rejected"; 6 | data: any; 7 | error: any; 8 | } 9 | 10 | interface QuestionsAnswerState { 11 | answers: Answer; 12 | } 13 | 14 | export const fetchQuestionsAnswers = createAsyncThunk( 15 | "fetch/QuestionsAnswers", 16 | async (id: string) => { 17 | axios 18 | .get(`/questions/${id}/answers`) 19 | .then((res) => res.data) 20 | .catch((error) => error); 21 | } 22 | ); 23 | export const updateAnswersVotes = createAsyncThunk( 24 | "fetch/QuestionsAnswers", 25 | async ({ questionId, answerId, vote }: { questionId: string, answerId: string, vote: boolean }, { getState }) => { 26 | const state = getState() as any; 27 | const access_token = state?.user?.user?.access_token; 28 | const url = `/questions/${questionId}/answers/${answerId}?${vote ? `action_type=upvote` : `action_type=downvote`}` 29 | const response = await axios.put(url, {}, { 30 | headers: { 31 | Authorization: `Bearer ${access_token}` 32 | } 33 | }); 34 | return response.data.questions; 35 | } 36 | ); 37 | 38 | 39 | 40 | const initialState = { 41 | answers: { 42 | status: "idle", 43 | data: [], 44 | error: null, 45 | }, 46 | } as QuestionsAnswerState; 47 | 48 | export const QuestionAnswerSlice = createSlice({ 49 | name: "answers", 50 | initialState: initialState, 51 | reducers: {}, 52 | extraReducers: { 53 | [fetchQuestionsAnswers.pending.type]: ( 54 | state: QuestionsAnswerState, 55 | action 56 | ) => { 57 | state.answers = { 58 | status: "pending", 59 | data: [], 60 | error: null, 61 | }; 62 | }, 63 | [fetchQuestionsAnswers.fulfilled.type]: ( 64 | state: QuestionsAnswerState, 65 | action 66 | ) => { 67 | state.answers = { 68 | status: "idle", 69 | data: action.payload, 70 | error: null, 71 | }; 72 | }, 73 | [fetchQuestionsAnswers.rejected.type]: ( 74 | state: QuestionsAnswerState, 75 | action 76 | ) => { 77 | state.answers = { 78 | status: "idle", 79 | data: [], 80 | error: action.payload, 81 | }; 82 | }, 83 | }, 84 | }); 85 | 86 | export const selectQuestions = (state: QuestionsAnswerState) => 87 | state.answers.data; 88 | export default QuestionAnswerSlice.reducer; 89 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/features/question.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; 2 | import axios from "axios"; 3 | export interface Question { 4 | status: "idle" | "pending" | "rejected"; 5 | data: any; 6 | error: any; 7 | } 8 | 9 | interface QuestionsState { 10 | question: Question; 11 | selectedQuestion: Question; 12 | } 13 | 14 | export const fetchQuestions = createAsyncThunk("fetch/Questions", async (tags?: string) => { 15 | const url = tags ? `/questions?tags=${tags}&page=1&limit=1000` : `/questions?page=1&limit=1000` 16 | const response = await axios.get(url); 17 | return response.data.questions; 18 | }); 19 | 20 | export const createQuestion = createAsyncThunk("fetch/CreateQuestions", 21 | async (payload: any, { getState }: any) => { 22 | const state = getState() as any; 23 | const access_token = state?.user?.user?.access_token; 24 | const response = await axios.post(`/questions`, payload, { 25 | headers: { 26 | Authorization: `Bearer ${access_token}` 27 | } 28 | }); 29 | return response.data; 30 | }); 31 | 32 | export const fetchQuestionsById = createAsyncThunk( 33 | "fetch/QuestionAnswers", 34 | async (id: string) => { 35 | const response = await axios.get(`/questions/${id}`); 36 | return response.data; 37 | } 38 | ); 39 | 40 | const initialState = { 41 | question: { 42 | status: "idle", 43 | data: [], 44 | error: null, 45 | }, 46 | selectedQuestion: { 47 | status: "idle", 48 | data: {}, 49 | error: null, 50 | }, 51 | } as QuestionsState; 52 | 53 | export const QuestionSlice = createSlice({ 54 | name: "questions", 55 | initialState: initialState, 56 | reducers: {}, 57 | extraReducers: { 58 | [fetchQuestions.pending.type]: (state: QuestionsState, action) => { 59 | state.question = { 60 | status: "pending", 61 | data: [], 62 | error: null, 63 | }; 64 | }, 65 | [fetchQuestions.fulfilled.type]: (state: QuestionsState, action) => { 66 | console.log(action); 67 | state.question = { 68 | status: "idle", 69 | data: action.payload, 70 | error: null, 71 | }; 72 | }, 73 | [fetchQuestions.rejected.type]: (state: QuestionsState, action) => { 74 | state.question = { 75 | status: "idle", 76 | data: [], 77 | error: action.payload, 78 | }; 79 | }, 80 | [fetchQuestionsById.pending.type]: (state: QuestionsState, action) => { 81 | console.log(state); 82 | state.selectedQuestion = { 83 | status: "pending", 84 | data: {}, 85 | error: null, 86 | }; 87 | }, 88 | [fetchQuestionsById.fulfilled.type]: (state: QuestionsState, action) => { 89 | console.log(state); 90 | console.log(action); 91 | state.selectedQuestion = { 92 | status: "idle", 93 | data: action.payload, 94 | error: null, 95 | }; 96 | }, 97 | [fetchQuestionsById.rejected.type]: (state: QuestionsState, action) => { 98 | state.selectedQuestion = { 99 | status: "idle", 100 | data: {}, 101 | error: action.payload, 102 | }; 103 | }, 104 | [createQuestion.pending.type]: (state: QuestionsState, action) => { 105 | return state; 106 | }, 107 | [createQuestion.fulfilled.type]: (state: QuestionsState, action) => { 108 | console.log(action); 109 | state.question.data.push(action.payload) 110 | }, 111 | [createQuestion.rejected.type]: (state: QuestionsState, action) => { 112 | return state; 113 | }, 114 | 115 | 116 | }, 117 | }); 118 | 119 | 120 | export const questionsSelector = (state: any) => state.questions.question.data; 121 | export const selectedQuestionSelector = (state: any) => state.questions.selectedQuestion.data; 122 | export default QuestionSlice.reducer; 123 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/features/user.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export interface User { 4 | userName: string; 5 | photo: string; 6 | email: string; 7 | uid: string; 8 | } 9 | 10 | interface UserState { 11 | user: User | null; 12 | } 13 | 14 | const initialState = { user: {} } as UserState; 15 | 16 | export const UserSlice = createSlice({ 17 | name: "user", 18 | initialState: initialState, 19 | reducers: { 20 | login: (state: UserState, action) => { 21 | state.user = action.payload; 22 | }, 23 | logout: (state: UserState) => { 24 | state.user = null; 25 | }, 26 | }, 27 | }); 28 | 29 | export const { login, logout } = UserSlice.actions; 30 | export const selectUser = (state: any) => state.user.user; 31 | export default UserSlice.reducer; 32 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/firebase.ts: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp } from "firebase/app"; 3 | import { getAuth, GoogleAuthProvider } from "firebase/auth"; 4 | // import { getAnalytics } from "firebase/analytics"; 5 | // TODO: Add SDKs for Firebase products that you want to use 6 | // https://firebase.google.com/docs/web/setup#available-libraries 7 | 8 | // Your web app's Firebase configuration 9 | // For Firebase JS SDK v7.20.0 and later, measurementId is optional 10 | 11 | const firebaseConfig = { 12 | apiKey: process.env.REACT_APP_PUBLIC_FIREBASE_KEY, 13 | authDomain: process.env.REACT_APP_PUBLIC_AUTH_DOMAIN, 14 | databaseURL: process.env.REACT_APP_PUBLIC_DATABASE_URL, 15 | projectId: process.env.REACT_APP_PUBLIC_PROJECTID, 16 | storageBucket: process.env.REACT_APP_PUBLIC_STORAGE_BUCKET, 17 | messagingSenderId: process.env.REACT_APP_PUBLIC_SENDER_ID, 18 | appId: process.env.REACT_APP_PUBLIC_APPID, 19 | measurementId: process.env.REACT_APP_PUBLIC_MEASUREMENT_ID, 20 | }; 21 | 22 | // Initialize Firebase 23 | const app = initializeApp(firebaseConfig); 24 | const auth = getAuth(); 25 | const provider = new GoogleAuthProvider(); 26 | 27 | export { auth, provider }; 28 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./app"; 5 | import { Provider } from "react-redux"; 6 | import store from "./store"; 7 | import { BrowserRouter, Routes, Route, Navigate, Link, useNavigate } from "react-router-dom"; 8 | 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById("root") 19 | ); 20 | 21 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import UserReducer from "../features/user.slice"; 3 | import QuestionReducer from "../features/question.slice"; 4 | import AnswerReducer from "../features/answer.slice"; 5 | 6 | export default configureStore({ 7 | reducer: { 8 | user: UserReducer, 9 | questions: QuestionReducer, 10 | answer: AnswerReducer 11 | }, 12 | devTools: true, 13 | }); 14 | -------------------------------------------------------------------------------- /apps/stack-overflow-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres-stackoverflow: 4 | environment: 5 | - POSTGRES_USER=api 6 | - POSTGRES_PASSWORD=development_pass 7 | - POSTGRES_MULTIPLE_DATABASES="overflow-api","overflow-api-testing" 8 | volumes: 9 | - ./docker-utils:/docker-entrypoint-initdb.d 10 | - sv_api_data:/data/postgres 11 | ports: 12 | - 5436:5432 13 | 14 | volumes: 15 | sv_api_data: {} 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | services: 3 | postgres-stackoverflow: 4 | image: postgres 5 | restart: unless-stopped 6 | -------------------------------------------------------------------------------- /docker-utils/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -u 4 | 5 | function create_user_and_database() { 6 | local database=$1 7 | echo " Creating user and database '$database'" 8 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 9 | CREATE USER $database; 10 | CREATE DATABASE $database; 11 | GRANT ALL PRIVILEGES ON DATABASE $database TO $database; 12 | EOSQL 13 | } 14 | 15 | if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then 16 | echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" 17 | for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do 18 | create_user_and_database $db 19 | done 20 | echo "Multiple databases created" 21 | fi 22 | 23 | -------------------------------------------------------------------------------- /docker-utils/entrypoint/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "*****" 5 | echo "** Application preparing to start up... Hi!" 6 | echo "** Local time :$(date -Is)" 7 | echo "** SERVICE_TITLE :${SERVICE_TITLE}" 8 | echo "** SERVICE_DESCRIPTION:${SERVICE_DESCRIPTION}" 9 | echo "** SERVICE_VERSION :${SERVICE_VERSION}" 10 | echo "** SERVICE_IDENTIFIER :${SERVICE_IDENTIFIER}" 11 | echo "** SERVICE_NAME :${SERVICE_NAME}" 12 | echo "*****" 13 | 14 | if [ -d "/app" ] 15 | then 16 | pushd /app 17 | 18 | if [ "$NPM_INSTALL" = "ENABLE" ] 19 | then 20 | echo "+Running npm install - disable with .env entry NPM_INSTALL=DISABLE" 21 | npm install 22 | npm run build 23 | else 24 | echo "+Skipping npm install - enable with .env entry NPM_INSTALL=ENABLE" 25 | fi 26 | 27 | if [ "$TYPEORM_MIGRATION" = "ENABLE" ] 28 | then 29 | echo "+Running typeorm migrations (caches will be cleared) - disable with .env entry TYPEORM_MIGRATION=DISABLE" 30 | npm run migration:run 31 | else 32 | echo "+Skipping typeorm migrations - enable with .env entry TYPEORM_MIGRATION=ENABLE" 33 | fi 34 | 35 | popd 36 | fi 37 | 38 | if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ]; then 39 | set -- node "$@" 40 | fi 41 | 42 | exec "$@" -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | testEnvironment: "node", 6 | restoreMocks: true, 7 | collectCoverageFrom: ["**/*.(t|j)s"], 8 | coverageDirectory: "../coverage", 9 | transform: { 10 | "^.+\\.ts$": "ts-jest", 11 | }, 12 | moduleFileExtensions: ["ts", "js", "json", "node"], 13 | }; 14 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "nx/presets/npm.json", 3 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 4 | "tasksRunnerOptions": { 5 | "default": { 6 | "runner": "nx/tasks-runners/default", 7 | "options": { 8 | "cacheableOperations": [ 9 | "lint", 10 | "test", 11 | "e2e" 12 | ] 13 | } 14 | } 15 | }, 16 | "targetDefaults": { 17 | "build": { 18 | "dependsOn": ["^build"] 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "build-all":"pnpm run --parallel -r build", 6 | "nx-build-all":"nx run-many --target=build", 7 | "lint": "node node_modules/prettier/bin-prettier --check \"**/*.{js,json,ts,yml,yaml}\"", 8 | "prepare": "husky install", 9 | "prettier": "node node_modules/prettier/bin-prettier --check '**/*.{js,json,ts,yml,yaml}'", 10 | "prettier:write": "node node_modules/prettier/bin-prettier --write \"**/*.{js,json,ts,yml,yaml}\"" 11 | }, 12 | "devDependencies": { 13 | "@commitlint/cli": "^16.0.1", 14 | "@commitlint/config-conventional": "^16.0.0", 15 | "@typescript-eslint/eslint-plugin": "^5.9.0", 16 | "@typescript-eslint/parser": "^5.9.0", 17 | "commitizen": "^4.2.4", 18 | "conventional-changelog-cli": "^2.2.2", 19 | "cz-conventional-changelog": "^3.3.0", 20 | "eslint": "^8.28.0", 21 | "eslint-config-prettier": "^8.5.0", 22 | "eslint-plugin-prettier": "^4.2.1", 23 | "fbjs-scripts": "^3.0.1", 24 | "husky": "^7.0.4", 25 | "jest": "^27.4.7", 26 | "nx": "^15.8.8", 27 | "prettier": "^2.8.6", 28 | "ts-jest": "^27.1.2", 29 | "typescript": "^4.5.4" 30 | }, 31 | "config": { 32 | "commitizen": { 33 | "path": "./node_modules/cz-conventional-changelog" 34 | } 35 | }, 36 | "dependencies": { 37 | "eslint-config-prettier": "^8.7.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/api-auth/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | const base = require("../../jest.config.js"); 4 | 5 | module.exports = { 6 | ...base, 7 | rootDir: "./build", 8 | name: "drivers", 9 | displayName: "@cdc3/drivers", 10 | collectCoverage: true, 11 | verbose: true, 12 | coverageThreshold: { 13 | global: { 14 | statements: 5, 15 | branches: 5, 16 | functions: 5, 17 | lines: 5, 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/api-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev/auth", 3 | "version": "1.0.0", 4 | "main": "./build/index.js", 5 | "types": "./build/index.d.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "tsc-version": "tsc --version", 9 | "check-engines": "node ../../node_modules/fbjs-scripts/node/check-dev-engines.js package.json", 10 | "clean": "rimraf build && rimraf coverage", 11 | "prebuild": "npm run clean && npm run check-engines", 12 | "prepublishOnly": "npm run build", 13 | "pretest": "npm run build", 14 | "test": "npm run test:ci", 15 | "test:ci": "jest --ci --collectCoverage --maxWorkers 2 --passWithNoTests" 16 | }, 17 | "engines": { 18 | "node": ">=14.x", 19 | "npm": ">=6.14.x" 20 | }, 21 | "devEngines": { 22 | "node": ">=14.x", 23 | "npm": ">=6.14.x" 24 | }, 25 | "dependencies": { 26 | "@dev/config": "1.0.0", 27 | "@nestjs/passport": "^9.0.3", 28 | "firebase-admin": "^11.6.0", 29 | "passport": "^0.6.0", 30 | "passport-firebase-jwt": "^1.2.1", 31 | "uuid": "^8.3.2" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/common": "^9.0.0", 35 | "@nestjs/config": "2.3.1", 36 | "@nestjs/core": "^9.0.0", 37 | "@nestjs/testing": "^9.0.0", 38 | "@types/passport":"^1.0.12", 39 | "@types/express": "^4.17.13", 40 | "@types/jest": "27.0.2", 41 | "@types/node": "^17.0.45", 42 | "@types/uuid": "^8.3.4", 43 | "express": "^4.18.2", 44 | "reflect-metadata": "^0.1.13", 45 | "rxjs": "^7.2.0", 46 | "tslib": "^2.5.0", 47 | "typescript": "^4.7.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/api-auth/src/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { AuthGuard } from "@nestjs/passport"; 3 | 4 | @Injectable() 5 | export class FirebaseAuthGuard extends AuthGuard("firebase-auth") {} 6 | -------------------------------------------------------------------------------- /packages/api-auth/src/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { PassportModule } from "@nestjs/passport"; 3 | import { FirebaseAuthStrategy } from "./auth.strategy"; 4 | import { ConfigModule } from "@dev/config"; 5 | @Module({ 6 | imports: [ 7 | ConfigModule, 8 | PassportModule.register({ defaultStrategy: "firebase-auth" }), 9 | ], 10 | providers: [FirebaseAuthStrategy], 11 | exports: [PassportModule, FirebaseAuthStrategy], 12 | }) 13 | export class AuthModule {} 14 | -------------------------------------------------------------------------------- /packages/api-auth/src/auth.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from "@nestjs/passport"; 2 | import { Injectable, UnauthorizedException } from "@nestjs/common"; 3 | import { Strategy, ExtractJwt } from "passport-firebase-jwt"; 4 | import { ConfigService } from "@dev/config"; 5 | import { auth } from "firebase-admin"; 6 | import { FirebaseInstance } from "./firebase.service"; 7 | 8 | @Injectable() 9 | export class FirebaseAuthStrategy extends PassportStrategy( 10 | Strategy, 11 | "firebase-auth" 12 | ) { 13 | constructor(private readonly configService: ConfigService) { 14 | super({ 15 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 16 | }); 17 | console.log(this.configService); 18 | } 19 | 20 | validate(token: string) { 21 | FirebaseInstance.Instance; 22 | const user = auth() 23 | .verifyIdToken(token, true) 24 | .catch((err: any) => { 25 | throw new UnauthorizedException(err); 26 | }); 27 | console.log(user); 28 | // req.user 29 | return user; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/api-auth/src/authz.decorator.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkssharma/stack-overflow-full-stack-clone/5fbfa5cf64413e23b83182793c590aabd94d9fbd/packages/api-auth/src/authz.decorator.ts -------------------------------------------------------------------------------- /packages/api-auth/src/firebase.service.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase-admin/app"; 2 | import admin from "firebase-admin"; 3 | 4 | export class FirebaseInstance { 5 | private static _instance: FirebaseInstance; 6 | 7 | private constructor() { 8 | return initializeApp({ 9 | credential: admin.credential.cert({ 10 | private_key: process.env.FIREBASE_PRIVATE_KEY, 11 | client_email: process.env.FIREBASE_CLIENT_EMAIL, 12 | project_id: process.env.FIREBASE_PROJECT_ID, 13 | } as Partial), 14 | databaseURL: process.env.FIREBASE_DATABASE_URL!, 15 | }); 16 | } 17 | 18 | public static get Instance() { 19 | // Do you need arguments? Make it a regular static method instead. 20 | return this._instance || (this._instance = new this()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/api-auth/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth.guard"; 2 | export * from "./auth.module"; 3 | export * from "./auth.strategy"; 4 | export * from "./firebase.service"; 5 | export * from "./user"; 6 | -------------------------------------------------------------------------------- /packages/api-auth/src/user.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from "@nestjs/common"; 2 | 3 | export const User = createParamDecorator( 4 | (_data: any, ctx: ExecutionContext) => { 5 | const req = ctx.switchToHttp().getRequest(); 6 | // if route is protected, there is a user set in auth.middleware 7 | if (req.user) { 8 | return req.user; 9 | } 10 | return null; 11 | } 12 | ); 13 | 14 | export interface UserMetaData { 15 | uid: string; 16 | email: string; 17 | } 18 | -------------------------------------------------------------------------------- /packages/api-auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ESNext"], 5 | "downlevelIteration": true, 6 | "importHelpers": true, 7 | "module": "commonjs", 8 | "esModuleInterop": true, 9 | "declaration": true, 10 | "strict": true, 11 | "pretty": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "experimentalDecorators": true, 16 | "outDir": "./build", 17 | "rootDir": "./src" 18 | }, 19 | "exclude": ["node_modules", "build"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/api-config/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | const base = require("../../jest.config.js"); 4 | 5 | module.exports = { 6 | ...base, 7 | rootDir: "./build", 8 | name: "drivers", 9 | displayName: "@cdc3/drivers", 10 | collectCoverage: true, 11 | verbose: true, 12 | coverageThreshold: { 13 | global: { 14 | statements: 5, 15 | branches: 5, 16 | functions: 5, 17 | lines: 5, 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/api-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev/config", 3 | "version": "1.0.0", 4 | "main": "./build/index.js", 5 | "types": "./build/index.d.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "check-engines": "node ../../node_modules/fbjs-scripts/node/check-dev-engines.js package.json", 9 | "clean": "rimraf build && rimraf coverage", 10 | "prebuild": "npm run clean && npm run check-engines", 11 | "prepublishOnly": "npm run build", 12 | "pretest": "npm run build", 13 | "test": "npm run test:ci", 14 | "test:ci": "jest --ci --collectCoverage --maxWorkers 2 --passWithNoTests" 15 | }, 16 | "engines": { 17 | "node": ">=14.x", 18 | "npm": ">=6.14.x" 19 | }, 20 | "devEngines": { 21 | "node": ">=14.x", 22 | "npm": ">=6.14.x" 23 | }, 24 | "dependencies": { 25 | "firebase-admin": "^11.6.0", 26 | "uuid": "^8.3.2" 27 | }, 28 | "devDependencies": { 29 | "@nestjs/common": "^9.0.0", 30 | "@nestjs/config": "2.3.1", 31 | "@nestjs/core": "^9.0.0", 32 | "@nestjs/testing": "^9.0.0", 33 | "@types/jest": "27.0.2", 34 | "@types/node": "^17.0.8", 35 | "@types/uuid": "^8.3.4", 36 | "reflect-metadata": "^0.1.13", 37 | "rxjs": "^7.2.0", 38 | "tslib": "^2.5.0", 39 | "typescript": "^4.7.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/api-config/src/config.default.ts: -------------------------------------------------------------------------------- 1 | import { ConfigData } from "./config.interface"; 2 | 3 | export const DEFAULT_CONFIG: ConfigData = { 4 | port: Number(process.env.PORT || 3001), 5 | env: "production", 6 | db: { 7 | url: process.env.DATABASE_URL!, 8 | }, 9 | auth: { 10 | expiresIn: 30000, 11 | access_token_secret: "", 12 | refresh_token_secret: "", 13 | }, 14 | swagger: { 15 | username: "", 16 | password: "", 17 | }, 18 | logLevel: "", 19 | }; 20 | -------------------------------------------------------------------------------- /packages/api-config/src/config.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ConfigDatabase { 2 | url: string; 3 | } 4 | 5 | export interface ConfigSwagger { 6 | username: string; 7 | password: string; 8 | } 9 | 10 | export interface AuthConfig { 11 | expiresIn: number; 12 | access_token_secret: string; 13 | refresh_token_secret: string; 14 | } 15 | 16 | export interface UserServiceConfig { 17 | options: UserServiceConfigOptions; 18 | transport: any; 19 | } 20 | 21 | export interface ElasticConfig { 22 | url: string; 23 | username?: string; 24 | password?: string; 25 | index?: string; 26 | } 27 | 28 | export interface UserServiceConfigOptions { 29 | host: string; 30 | port: number; 31 | } 32 | 33 | export interface FirebaseConfig { 34 | databaseURL?: string; 35 | credential?: any; 36 | } 37 | 38 | export interface ConfigData { 39 | env: string; 40 | 41 | port: number; 42 | 43 | db: ConfigDatabase; 44 | 45 | swagger: ConfigSwagger; 46 | 47 | logLevel: string; 48 | 49 | auth: AuthConfig; 50 | 51 | firebase?: FirebaseConfig; 52 | } 53 | -------------------------------------------------------------------------------- /packages/api-config/src/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { ConfigService } from "./config.service"; 3 | 4 | const configFactory = { 5 | provide: ConfigService, 6 | useFactory: () => { 7 | const config = new ConfigService(); 8 | config.loadFromEnv(); 9 | return config; 10 | }, 11 | }; 12 | 13 | @Module({ 14 | imports: [], 15 | controllers: [], 16 | providers: [configFactory], 17 | exports: [configFactory], 18 | }) 19 | export class ConfigModule {} 20 | -------------------------------------------------------------------------------- /packages/api-config/src/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { DEFAULT_CONFIG } from "./config.default"; 3 | import { ConfigData, ConfigDatabase, ConfigSwagger } from "./config.interface"; 4 | import * as admin from "firebase-admin"; 5 | 6 | @Injectable() 7 | export class ConfigService { 8 | private config: ConfigData; 9 | constructor(data: ConfigData = DEFAULT_CONFIG) { 10 | this.config = data; 11 | } 12 | 13 | public loadFromEnv() { 14 | this.config = this.parseConfigFromEnv(process.env); 15 | } 16 | 17 | private parseConfigFromEnv(env: NodeJS.ProcessEnv): ConfigData { 18 | return { 19 | env: env.NODE_ENV || DEFAULT_CONFIG.env, 20 | port: parseInt(env.PORT!, 10), 21 | db: this.parseDBConfig(env, DEFAULT_CONFIG.db), 22 | swagger: this.parseSwaggerConfig(env, DEFAULT_CONFIG.swagger), 23 | logLevel: env.LOG_LEVEL!, 24 | auth: { 25 | expiresIn: Number(env.TOKEN_EXPIRY), 26 | access_token_secret: env.JWT_ACCESS_TOKEN_SECRET!, 27 | refresh_token_secret: env.JWT_REFRESH_TOKEN_SECRET!, 28 | }, 29 | firebase: { 30 | credential: admin.credential.cert({ 31 | private_key: process.env.FIREBASE_PRIVATE_KEY, 32 | client_email: process.env.FIREBASE_CLIENT_EMAIL, 33 | project_id: process.env.FIREBASE_PROJECT_ID, 34 | } as Partial), 35 | databaseURL: process.env.FIREBASE_DATABASE_URL!, 36 | }, 37 | }; 38 | } 39 | private parseDBConfig( 40 | env: NodeJS.ProcessEnv, 41 | defaultConfig: Readonly 42 | ) { 43 | return { 44 | url: env.DATABASE_URL || defaultConfig.url, 45 | }; 46 | } 47 | private parseSwaggerConfig( 48 | env: NodeJS.ProcessEnv, 49 | defaultConfig: Readonly 50 | ) { 51 | return { 52 | username: env.SWAGGER_USERNAME || defaultConfig.username, 53 | password: env.SWAGGER_PASSWORD || defaultConfig.password, 54 | }; 55 | } 56 | 57 | public get(): Readonly { 58 | return this.config; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/api-config/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./config.module"; 2 | export * from "./config.service"; 3 | export * from "./config.interface"; 4 | export * from "./config.default"; 5 | -------------------------------------------------------------------------------- /packages/api-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ESNext"], 5 | "downlevelIteration": true, 6 | "importHelpers": true, 7 | "module": "commonjs", 8 | "declaration": true, 9 | "strict": true, 10 | "pretty": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "experimentalDecorators": true, 15 | "outDir": "./build", 16 | "rootDir": "./src" 17 | }, 18 | "exclude": ["node_modules", "build"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/api-database/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | const base = require("../../jest.config.js"); 4 | 5 | module.exports = { 6 | ...base, 7 | rootDir: "./build", 8 | name: "drivers", 9 | displayName: "@cdc3/drivers", 10 | collectCoverage: true, 11 | verbose: true, 12 | coverageThreshold: { 13 | global: { 14 | statements: 5, 15 | branches: 5, 16 | functions: 5, 17 | lines: 5, 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/api-database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev/database", 3 | "version": "1.0.0", 4 | "main": "./build/index.js", 5 | "types": "./build/index.d.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "tsc-version": "tsc --version", 9 | "check-engines": "node ../../node_modules/fbjs-scripts/node/check-dev-engines.js package.json", 10 | "clean": "rimraf build && rimraf coverage", 11 | "prebuild": "npm run clean && npm run check-engines", 12 | "prepublishOnly": "npm run build", 13 | "pretest": "npm run build", 14 | "test": "npm run test:ci", 15 | "test:ci": "jest --ci --collectCoverage --maxWorkers 2 --passWithNoTests" 16 | }, 17 | "engines": { 18 | "node": ">=14.x", 19 | "npm": ">=6.14.x" 20 | }, 21 | "devEngines": { 22 | "node": ">=14.x", 23 | "npm": ">=6.14.x" 24 | }, 25 | "dependencies": { 26 | "@dev/config": "1.0.0", 27 | "@nestjs/typeorm": "^9.0.1", 28 | "typeorm": "^0.3.12" 29 | }, 30 | "devDependencies": { 31 | "@nestjs/common": "^9.0.0", 32 | "@nestjs/config": "2.3.1", 33 | "@nestjs/core": "^9.0.0", 34 | "@nestjs/testing": "^9.0.0", 35 | "@types/express": "^4.17.13", 36 | "@types/jest": "27.0.2", 37 | "@types/node": "^17.0.45", 38 | "@types/uuid": "^8.3.4", 39 | "express": "^4.18.2", 40 | "reflect-metadata": "^0.1.13", 41 | "rxjs": "^7.2.0", 42 | "tslib": "^2.5.0", 43 | "typescript": "^4.7.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/api-database/src/db.interface.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions } from "typeorm"; 2 | 3 | export interface DbConfig { 4 | entities: ConnectionOptions["entities"]; 5 | } 6 | -------------------------------------------------------------------------------- /packages/api-database/src/db.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { DbConfig } from "./db.interface"; 3 | import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm"; 4 | import { ConfigModule } from "@dev/config"; 5 | import { ConfigService } from "@dev/config"; 6 | import { ConfigDatabase } from "@dev/config"; 7 | 8 | @Module({}) 9 | export class DBModule { 10 | private static getConnectionOptions( 11 | config: ConfigService, 12 | dbConfig: DbConfig 13 | ): TypeOrmModuleOptions { 14 | const dbData = config.get().db; 15 | if (!dbData) { 16 | throw Error(""); 17 | } 18 | const connectionOptions = this.getConnectionOptionsPostgres(dbData); 19 | return { 20 | ...connectionOptions, 21 | entities: dbConfig.entities, 22 | synchronize: true, 23 | logging: true, 24 | }; 25 | } 26 | 27 | private static getConnectionOptionsPostgres( 28 | dbData: ConfigDatabase 29 | ): TypeOrmModuleOptions { 30 | return { 31 | type: "postgres", 32 | url: dbData.url, 33 | keepConnectionAlive: true, 34 | ssl: 35 | process.env.NODE_ENV !== "local" && process.env.NODE_ENV !== "test" 36 | ? { rejectUnauthorized: false } 37 | : false, 38 | }; 39 | } 40 | 41 | public static forRoot(dbConfig: DbConfig) { 42 | return { 43 | module: DBModule, 44 | imports: [ 45 | TypeOrmModule.forRootAsync({ 46 | imports: [ConfigModule], 47 | useFactory: (configService: ConfigService) => { 48 | return DBModule.getConnectionOptions(configService, dbConfig); 49 | }, 50 | inject: [ConfigService], 51 | }), 52 | ], 53 | controllers: [], 54 | providers: [], 55 | exports: [], 56 | }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/api-database/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./db.interface"; 2 | export * from "./db.module"; 3 | -------------------------------------------------------------------------------- /packages/api-database/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ESNext"], 5 | "downlevelIteration": true, 6 | "importHelpers": true, 7 | "module": "commonjs", 8 | "declaration": true, 9 | "strict": true, 10 | "pretty": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "experimentalDecorators": true, 15 | "outDir": "./build", 16 | "rootDir": "./src" 17 | }, 18 | "exclude": ["node_modules", "build"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/api-types/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | const base = require("../../jest.config.js"); 4 | 5 | module.exports = { 6 | ...base, 7 | rootDir: "./build", 8 | name: "drivers", 9 | collectCoverage: true, 10 | verbose: true, 11 | coverageThreshold: { 12 | global: { 13 | statements: 5, 14 | branches: 5, 15 | functions: 5, 16 | lines: 5, 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/api-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev/types", 3 | "version": "1.0.0", 4 | "main": "./build/index.js", 5 | "types": "./build/index.d.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "check-engines": "node ../../node_modules/fbjs-scripts/node/check-dev-engines.js package.json", 9 | "clean": "rimraf build && rimraf coverage", 10 | "prebuild": "npm run clean && npm run check-engines", 11 | "prepublishOnly": "npm run build", 12 | "pretest": "npm run build", 13 | "test": "npm run test:ci", 14 | "test:ci": "jest --ci --collectCoverage --maxWorkers 2 --passWithNoTests" 15 | }, 16 | "engines": { 17 | "node": ">=14.x", 18 | "npm": ">=6.14.x" 19 | }, 20 | "devEngines": { 21 | "node": ">=14.x", 22 | "npm": ">=6.14.x" 23 | }, 24 | "dependencies": { 25 | "uuid": "^8.3.2" 26 | }, 27 | "devDependencies": { 28 | "@types/jest": "27.0.2", 29 | "@types/node": "^17.0.45", 30 | "@types/uuid": "^8.3.4", 31 | "express": "^4.18.2", 32 | "tslib": "^2.5.0", 33 | "typescript": "4.7.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/api-types/src/index.ts: -------------------------------------------------------------------------------- 1 | export class Hello { 2 | constructor() {} 3 | 4 | async sayHi() { 5 | return Promise.resolve(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/api-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ESNext"], 5 | "downlevelIteration": true, 6 | "importHelpers": true, 7 | "module": "commonjs", 8 | "declaration": true, 9 | "strict": true, 10 | "pretty": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "experimentalDecorators": true, 15 | "outDir": "./build", 16 | "rootDir": "./src" 17 | }, 18 | "exclude": ["node_modules", "build"] 19 | } 20 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in direct subdirs of packages/ 3 | - "packages/*" 4 | - "apps/*" 5 | - "infra/*" 6 | -------------------------------------------------------------------------------- /screens/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkssharma/stack-overflow-full-stack-clone/5fbfa5cf64413e23b83182793c590aabd94d9fbd/screens/main.png -------------------------------------------------------------------------------- /screens/main2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkssharma/stack-overflow-full-stack-clone/5fbfa5cf64413e23b83182793c590aabd94d9fbd/screens/main2.png -------------------------------------------------------------------------------- /screens/main3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkssharma/stack-overflow-full-stack-clone/5fbfa5cf64413e23b83182793c590aabd94d9fbd/screens/main3.png --------------------------------------------------------------------------------