├── .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 | 
30 | 
31 | 
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 |
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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |

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 |

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 |

56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
67 |
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 |

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
--------------------------------------------------------------------------------