├── .gitattributes ├── .github └── workflows │ └── ci-validation.yml ├── .gitignore ├── .run ├── Angular classic unit test.run.xml ├── Angular cypress unit test.run.xml ├── Angular serve.run.xml ├── ChatApiApplication.run.xml ├── Cypress open.run.xml ├── Cypress run.run.xml ├── React serve .run.xml └── Vue serve.run.xml ├── LICENSE ├── README.md ├── angular-chat ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── cypress.config.ts ├── cypress │ ├── fixtures │ │ └── example.json │ └── support │ │ ├── commands.ts │ │ ├── component-index.html │ │ └── component.ts ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── app.component.cy.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.compponent.spec.ts │ │ ├── app.module.ts │ │ ├── messages │ │ │ ├── message.component.cy.ts │ │ │ ├── message.interface.ts │ │ │ ├── message.service.ts │ │ │ ├── messages.component.html │ │ │ ├── messages.component.scss │ │ │ └── messages.component.ts │ │ └── user │ │ │ ├── user.interface.ts │ │ │ └── user.service.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ └── styles.scss ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json ├── chat-api ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── wandrillecorp │ │ │ └── chatapi │ │ │ ├── ChatApiApplication.java │ │ │ ├── api │ │ │ ├── WebSocketConfig.java │ │ │ ├── message │ │ │ │ ├── MessageController.java │ │ │ │ ├── MessageCreatedCommand.java │ │ │ │ └── WebSocketMessage.java │ │ │ └── user │ │ │ │ ├── UserController.java │ │ │ │ └── UserCreatedCommand.java │ │ │ ├── application │ │ │ ├── MessageManager.java │ │ │ └── UserManager.java │ │ │ ├── domain │ │ │ ├── ValueObject.java │ │ │ ├── bus │ │ │ │ └── ChatMessageBus.java │ │ │ ├── message │ │ │ │ ├── Message.java │ │ │ │ └── MessageRepository.java │ │ │ └── user │ │ │ │ ├── User.java │ │ │ │ └── UserRepository.java │ │ │ └── infrastructure │ │ │ ├── messageBus │ │ │ ├── KafkaEmission.java │ │ │ └── KafkaEventHandler.java │ │ │ └── repository │ │ │ ├── message │ │ │ ├── DefaultMongoMessageRepository.java │ │ │ └── MongoMessageRepository.java │ │ │ └── user │ │ │ ├── DefaultMongoUserRepository.java │ │ │ └── MongoUserRepository.java │ └── resources │ │ ├── application.yml │ │ └── avro │ │ └── message.avsc │ └── test │ └── java │ └── com │ └── wandrillecorp │ └── chatapi │ └── ChatApiApplicationTests.java ├── deploy ├── deploy.iml └── docker-compose.yml ├── doc ├── message_screen.jpg └── user_screen.jpg ├── e2e-tests ├── .cypress-cucumber-preprocessorrc.json ├── .gitignore ├── cypress.config.ts ├── cypress │ ├── e2e │ │ ├── chat-access.feature │ │ ├── chat-access.ts │ │ ├── common-step-definitions │ │ │ ├── common.ts │ │ │ └── system.ts │ │ ├── messages.feature │ │ └── messages.ts │ ├── fixtures │ │ └── example.json │ └── support │ │ ├── commands.ts │ │ └── e2e.ts ├── package-lock.json ├── package.json └── tsconfig.json ├── react-chat ├── .eslintrc.json ├── .gitignore ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── next.svg │ └── vercel.svg ├── src │ ├── interface │ │ ├── message.interface.ts │ │ └── user.interface.ts │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── index.tsx │ │ └── messages │ │ │ └── Messages.tsx │ └── styles │ │ ├── Message.module.scss │ │ └── globals.scss └── tsconfig.json └── vue-chat ├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.json ├── README.md ├── env.d.ts ├── index.html ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── Messages.vue │ ├── __tests__ │ │ └── Messages.spec.ts │ └── message.interface.ts ├── main.ts └── user.interface.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vitest.json ├── vite.config.ts └── vitest.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/ci-validation.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: CI Deployment Github Action 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | branches: [ "test-ci", "master" ] 10 | 11 | jobs: 12 | Check-Angular: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Use Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | cache: 'npm' 20 | cache-dependency-path: | 21 | angular-chat/package-lock.json 22 | - name: Install 23 | run: npm ci 24 | working-directory: angular-chat 25 | - name: Build 26 | run: npm run build 27 | working-directory: angular-chat 28 | - name: Test karma 29 | run: npm run test:prod 30 | working-directory: angular-chat 31 | - name: Test Cypress 32 | run: npm run test:cypress 33 | working-directory: angular-chat 34 | Check-React: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v3 38 | - name: Use Node.js 39 | uses: actions/setup-node@v3 40 | with: 41 | cache: 'npm' 42 | cache-dependency-path: | 43 | react-chat/package-lock.json 44 | - name: Install 45 | run: npm ci 46 | working-directory: react-chat 47 | - name: Build 48 | run: npm run build 49 | working-directory: react-chat 50 | Check-Vue: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v3 54 | - name: Use Node.js 55 | uses: actions/setup-node@v3 56 | with: 57 | cache: 'npm' 58 | cache-dependency-path: | 59 | vue-chat/package-lock.json 60 | - name: Install 61 | run: npm ci 62 | working-directory: vue-chat 63 | - name: Build 64 | run: npm run build 65 | working-directory: vue-chat 66 | - name: Test 67 | run: npm run test:unit 68 | working-directory: vue-chat 69 | Run-E2E: 70 | needs: [ Check-Vue,Check-Angular,Check-React ] 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/checkout@v3 74 | - name: Use Node.js 75 | uses: actions/setup-node@v3 76 | with: 77 | cache: 'npm' 78 | cache-dependency-path: | 79 | vue-chat/package-lock.json 80 | react-chat/package-lock.json 81 | angular-chat/package-lock.json 82 | - name: Use Java 83 | uses: actions/setup-java@v3 84 | with: 85 | distribution: 'adopt' 86 | cache: 'maven' 87 | java-version: '17' 88 | - name: Start Angular 89 | run: | 90 | cd angular-chat && 91 | npm ci && 92 | npm run start & 93 | sleep 5 94 | - name: Start React 95 | run: | 96 | cd react-chat && 97 | npm ci && 98 | npm run dev & 99 | sleep 5 100 | - name: Start React 101 | run: | 102 | cd vue-chat && 103 | npm ci && 104 | npm run dev & 105 | sleep 5 106 | - name: Run docker-compose 107 | run: | 108 | cd deploy && 109 | docker compose -f docker-compose.yml up -d 110 | - name: Install mvn packages 111 | run: | 112 | cd chat-api 113 | mvn compile 114 | - name: Start backend 115 | run: | 116 | cd chat-api 117 | mvn spring-boot:run & 118 | sleep 5 119 | - name: Install and run cypress 120 | run: | 121 | cd e2e-tests 122 | npm ci 123 | npm run start 124 | 125 | 126 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/* 3 | target 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.run/Angular classic unit test.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.run/Angular cypress unit test.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.run/Angular serve.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.run/ChatApiApplication.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.run/Cypress open.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.run/Cypress run.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.run/React serve .run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.run/Vue serve.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Wandrille Guesdon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chat : Full-stack coding exercise 2 | 3 | ## Presentation 4 | 5 | This application is a Whatsapp-like application. It provides realtime conversations with other people. 6 | 7 |  8 |  9 | ## Structure 10 | 11 | ### API 12 | The api is done with Java 8 and Spring Boot 2. 13 | It provides a websocket connection for realtime conversation. 14 | 15 | ### Multiple Fronts 16 | - [Angular](https://angular.io) (Typescript & scss) 17 | 18 | - [React](https://reactjs.org/) (Typescript & scss) 19 | 20 | - [Vue](https://fr.vuejs.org/) (Typescript & scss) 21 | 22 | ## Initiate the stack locally 23 | 24 | ### Database 25 | 26 | For the project, we will launch a docker image of mongoDB and Kafka. 27 | 28 | We can achieve to code a chat without any store or message broker. But if we do so we lose, to my mind, all the interest of the project. 29 | 30 | 1. Install [Docker](https://docs.docker.com/install/) 31 | 2. Install [docker-compose](https://docs.docker.com/compose/install/) 32 | 3. To run Kafka and Mongo images, execute in the deploy folder: 33 | ``` 34 | docker compose -f docker-compose.yml up -d 35 | ``` 36 | ### API 37 | 38 | 1. Install Java 8 and [maven](https://maven.apache.org/install.html) 39 | 2. Generate the maven packages and the Avro file with : 40 | ``` 41 | mvn compile 42 | ``` 43 | 44 | ### Front 45 | 46 | 1. Install [NodeJs](https://nodejs.org/en/) 47 | 2. For Angular, install the dependencies : 48 | ``` 49 | npm install -g @angular/cli 50 | ``` 51 | 3. Install the module of the project in the front folders: 52 | ``` 53 | npm i 54 | ``` 55 | 56 | ## How to start locally 57 | 58 | ### API 59 | 60 | Launch the API with : 61 | 62 | ``` 63 | mvn spring-boot:run 64 | ``` 65 | 66 | ### Front 67 | 68 | #### Angular 69 | 70 | Run the application in localhost:4200 in the front folder : 71 | ``` 72 | ng serve -o 73 | ``` 74 | 75 | #### React 76 | 77 | Run the application in localhost:3000 in the front folder : 78 | ``` 79 | npm start 80 | ``` 81 | 82 | #### Vue 83 | 84 | Run the application in localhost:5173 in the front folder : 85 | ``` 86 | npm run serve 87 | ``` 88 | 89 | ## What can be better : 90 | 91 | - The api is not completely independent. It will be better if we don't use `User` and `Message` from the domain. 92 | We need to create new classes for the API and map `Domain` and `User` to them. 93 | - For the purpose of the project, there is **NO test**. And this is a bad practice. We need to improve the test coverage. 94 | - The format of messages is known. It could be interesting to check if SQL can be better. 95 | - I use Angular to iterate quickly. But For better performance, it can be done without any framework, in pure JS. 96 | -------------------------------------------------------------------------------- /angular-chat/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /angular-chat/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # SystemEnum Files 45 | .DS_Store 46 | Thumbs.db 47 | /.angular/ 48 | /cypress/videos/app.component.cy.ts.mp4 49 | /cypress/videos/messages/message.component.cy.ts.mp4 50 | -------------------------------------------------------------------------------- /angular-chat/README.md: -------------------------------------------------------------------------------- 1 | # AngularChats 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.1.7. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /angular-chat/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-chat": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/angular-chat", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "src/styles.scss" 32 | ], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "fileReplacements": [ 38 | { 39 | "replace": "src/environments/environment.ts", 40 | "with": "src/environments/environment.prod.ts" 41 | } 42 | ], 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "sourceMap": false, 46 | "extractCss": true, 47 | "namedChunks": false, 48 | "extractLicenses": true, 49 | "vendorChunk": false, 50 | "buildOptimizer": true, 51 | "budgets": [ 52 | { 53 | "type": "initial", 54 | "maximumWarning": "2mb", 55 | "maximumError": "5mb" 56 | }, 57 | { 58 | "type": "anyComponentStyle", 59 | "maximumWarning": "6kb", 60 | "maximumError": "10kb" 61 | } 62 | ] 63 | } 64 | } 65 | }, 66 | "serve": { 67 | "builder": "@angular-devkit/build-angular:dev-server", 68 | "options": { 69 | "browserTarget": "angular-chat:build" 70 | }, 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "angular-chat:build:production" 74 | } 75 | } 76 | }, 77 | "extract-i18n": { 78 | "builder": "@angular-devkit/build-angular:extract-i18n", 79 | "options": { 80 | "browserTarget": "angular-chat:build" 81 | } 82 | }, 83 | "test": { 84 | "builder": "@angular-devkit/build-angular:karma", 85 | "options": { 86 | "polyfills": [ 87 | "zone.js", 88 | "zone.js/testing" 89 | ], 90 | "tsConfig": "tsconfig.spec.json", 91 | "inlineStyleLanguage": "scss", 92 | "assets": [ 93 | "src/favicon.ico", 94 | "src/assets" 95 | ], 96 | "styles": [ 97 | "src/styles.scss" 98 | ], 99 | "scripts": [] 100 | } 101 | }, 102 | "lint": { 103 | "builder": "@angular-devkit/build-angular:tslint", 104 | "options": { 105 | "tsConfig": [ 106 | "tsconfig.app.json", 107 | "tsconfig.spec.json", 108 | "e2e/tsconfig.json" 109 | ], 110 | "exclude": [ 111 | "**/node_modules/**" 112 | ] 113 | } 114 | }, 115 | "e2e": { 116 | "builder": "@angular-devkit/build-angular:protractor", 117 | "options": { 118 | "protractorConfig": "e2e/protractor.conf.js", 119 | "devServerTarget": "angular-chat:serve" 120 | }, 121 | "configurations": { 122 | "production": { 123 | "devServerTarget": "angular-chat:serve:production" 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /angular-chat/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | component: { 5 | devServer: { 6 | framework: "angular", 7 | bundler: "webpack", 8 | }, 9 | specPattern: "**/*.cy.ts", 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /angular-chat/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /angular-chat/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /angular-chat/cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /angular-chat/cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/angular' 23 | 24 | // Augment the Cypress namespace to include type definitions for 25 | // your custom command. 26 | // Alternatively, can be defined in cypress/support/component.d.ts 27 | // with a at the top of your spec. 28 | declare global { 29 | namespace Cypress { 30 | interface Chainable { 31 | mount: typeof mount 32 | } 33 | } 34 | } 35 | 36 | Cypress.Commands.add('mount', mount) 37 | 38 | // Example use: 39 | // cy.mount(MyComponent) -------------------------------------------------------------------------------- /angular-chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-chat", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "test:prod": "ng test --browsers=ChromeHeadless --watch=false --code-coverage", 11 | "test:cypress": "npx cypress run --component" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^16.0.0", 16 | "@angular/common": "^16.0.0", 17 | "@angular/compiler": "^16.0.0", 18 | "@angular/core": "^16.0.0", 19 | "@angular/forms": "^16.0.0", 20 | "@angular/platform-browser": "^16.0.0", 21 | "@angular/platform-browser-dynamic": "^16.0.0", 22 | "@angular/router": "^16.0.0", 23 | "rxjs": "^7.8.0", 24 | "tslib": "^2.3.0", 25 | "@stomp/stompjs": "^7.0.0", 26 | "zone.js": "~0.13.0" 27 | }, 28 | "devDependencies": { 29 | "@angular-devkit/build-angular": "^16.0.0", 30 | "@angular/cli": "^16.0.0", 31 | "@angular/compiler-cli": "^16.0.0", 32 | "@types/jasmine": "~4.3.1", 33 | "@types/node": "^18.14.6", 34 | "cypress": "^12.11.0", 35 | "jasmine-core": "~4.5.0", 36 | "karma": "~6.4.1", 37 | "karma-chrome-launcher": "~3.1.1", 38 | "karma-coverage": "~2.2.0", 39 | "karma-jasmine": "~5.1.0", 40 | "karma-jasmine-html-reporter": "~2.0.0", 41 | "ng-mocks": "^14.10.0", 42 | "typescript": "~4.9.5" 43 | } 44 | } -------------------------------------------------------------------------------- /angular-chat/src/app/app.component.cy.ts: -------------------------------------------------------------------------------- 1 | import { AppComponent } from "./app.component"; 2 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 3 | import { CommonModule } from "@angular/common"; 4 | import { UserService } from "./user/user.service"; 5 | import { MessagesComponent } from "./messages/messages.component"; 6 | import { MockComponent, MockProvider } from "ng-mocks"; 7 | import { User } from "./user/user.interface"; 8 | 9 | const user: User = { 10 | name: "michael", 11 | id: "some-id" 12 | }; 13 | 14 | const imports = [CommonModule, FormsModule, ReactiveFormsModule]; 15 | const providers = [MockProvider(UserService, { getUser: () => Promise.resolve(user) })]; 16 | const config = { 17 | providers, 18 | imports, 19 | declarations: [MockComponent(MessagesComponent)] 20 | }; 21 | 22 | describe("App component", () => { 23 | it("mounts", () => { 24 | cy.mount(AppComponent, config); 25 | }); 26 | 27 | it("should not display the message by default", () => { 28 | cy.mount(AppComponent, config); 29 | cy.get("app-messages").should("not.exist"); 30 | }); 31 | 32 | it("The input should be focused by default", () => { 33 | cy.mount(AppComponent, config); 34 | cy.get("input").should("be.focused"); 35 | }); 36 | 37 | it("should display the action button when a name is written", () => { 38 | cy.mount(AppComponent, config); 39 | cy.get(`button[type="submit"]`).should("not.exist"); 40 | cy.get("input").type("mike"); 41 | cy.get(`button[type="submit"]`).should("exist"); 42 | }); 43 | 44 | describe("When clicking on the action button, when the name is filled", () => { 45 | const name = "Mike"; 46 | beforeEach(() => { 47 | cy.mount(AppComponent, config).then((wrapper) => { 48 | cy.spy((wrapper.component as any).userService, "getUser").as("getUser"); 49 | return cy.wrap(wrapper).as("angular"); 50 | }); 51 | cy.get("input").type(name); 52 | cy.get(`button[type="submit"]`).click(); 53 | }); 54 | 55 | it("should send a request with the name ", () => { 56 | cy.get(`@getUser`).should("have.been.calledWith", name); 57 | }); 58 | 59 | it("should not display the user form but the message instead", () => { 60 | cy.get(`app-messages`).should("exist"); 61 | cy.get(`.userForm`).should("not.exist"); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /angular-chat/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to the chat 7 | 8 | 9 | What is your name ? 10 | 11 | 12 | 13 | 14 | Let's go! 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /angular-chat/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .application-component { 2 | width: 100%; 3 | height: 100%; 4 | min-width: 100%; 5 | min-height: 100%; 6 | display: flex; 7 | flex-direction: row; 8 | justify-content: center; 9 | align-items: center; 10 | align-content: center; 11 | 12 | &::before { 13 | content: ''; 14 | width: 100%; 15 | height: 200px; 16 | position: absolute; 17 | background-color: #3798D4; 18 | top: 0; 19 | z-index: -1; 20 | } 21 | 22 | .application-container { 23 | display: flex; 24 | flex-direction: row; 25 | justify-content: center; 26 | align-items: center; 27 | align-content: center; 28 | border-radius: 4px; 29 | box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 2px 5px 0 rgba(0, 0, 0, 0.2); 30 | background-color: #ffffff; 31 | font-size: 24px; 32 | 33 | @media (min-width: 1919px) { 34 | width: 60%; 35 | height: 90%; 36 | } 37 | 38 | .userForm { 39 | padding: 24px; 40 | display: flex; 41 | flex-direction: column; 42 | justify-content: center; 43 | align-items: center; 44 | align-content: center; 45 | 46 | .marginBottom15 { 47 | margin-bottom: 15px; 48 | } 49 | 50 | 51 | h1 { 52 | @media (max-width: 1200px) { 53 | font-size: 30px; 54 | } 55 | } 56 | 57 | form { 58 | display: flex; 59 | flex-direction: column; 60 | justify-content: center; 61 | align-items: center; 62 | align-content: center; 63 | 64 | input { 65 | font-size: 22px; 66 | width: 300px; 67 | border: 2px solid #aaa; 68 | border-radius: 4px; 69 | margin: 8px 0; 70 | outline: none; 71 | padding: 15px; 72 | box-sizing: border-box; 73 | transition: .3s; 74 | } 75 | 76 | input[type=text]:focus { 77 | border-color: dodgerBlue; 78 | box-shadow: 0 0 8px 0 dodgerBlue; 79 | } 80 | 81 | button { 82 | vertical-align: middle; 83 | padding: 10px 20px; 84 | font-size: 22px; 85 | color: #ffffff; 86 | text-decoration: none; 87 | background-color: #3798D4; 88 | text-align: center; 89 | letter-spacing: .5px; 90 | height: 50px; 91 | line-height: 36px; 92 | text-transform: uppercase; 93 | border: none; 94 | border-radius: 2px; 95 | outline: 0; 96 | position: relative; 97 | cursor: pointer; 98 | display: inline-block; 99 | overflow: hidden; 100 | } 101 | 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /angular-chat/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, ElementRef, ViewChild } from "@angular/core"; 2 | import { FormControl } from "@angular/forms"; 3 | import { UserService } from "./user/user.service"; 4 | import { User } from "./user/user.interface"; 5 | 6 | @Component({ 7 | selector: "app-root", 8 | templateUrl: "./app.component.html", 9 | styleUrls: ["./app.component.scss"] 10 | }) 11 | export class AppComponent implements AfterViewInit { 12 | 13 | @ViewChild("userInput") 14 | userInput: ElementRef; 15 | 16 | userName = new FormControl(""); 17 | 18 | user: User; 19 | 20 | constructor(private userService: UserService) { 21 | } 22 | 23 | ngAfterViewInit(): void { 24 | setTimeout(() => this.userInput.nativeElement.focus()); 25 | } 26 | 27 | activateUser() { 28 | if (this.userName.value && this.userName.value !== "") { 29 | this.userService.getUser(this.userName.value) 30 | .then(user => this.user = user); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /angular-chat/src/app/app.compponent.spec.ts: -------------------------------------------------------------------------------- 1 | import { AppComponent } from "./app.component"; 2 | import { TestBed } from "@angular/core/testing"; 3 | import { HttpClientTestingModule } from "@angular/common/http/testing"; 4 | 5 | describe("AppComponent", () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [HttpClientTestingModule], 9 | declarations: [ 10 | AppComponent 11 | ] 12 | }).compileComponents(); 13 | }); 14 | 15 | it("should create the app", () => { 16 | const fixture = TestBed.createComponent(AppComponent); 17 | const app = fixture.componentInstance; 18 | // @ts-ignore 19 | expect(app).toBeTruthy(); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /angular-chat/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | 4 | import {AppComponent} from './app.component'; 5 | import {HttpClientModule} from '@angular/common/http'; 6 | import {MessagesComponent} from './messages/messages.component'; 7 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 8 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | AppComponent, 13 | MessagesComponent 14 | ], 15 | imports: [ 16 | BrowserModule, 17 | HttpClientModule, 18 | ReactiveFormsModule, 19 | FormsModule, 20 | BrowserAnimationsModule 21 | ], 22 | providers: [], 23 | bootstrap: [AppComponent] 24 | }) 25 | export class AppModule { 26 | } 27 | -------------------------------------------------------------------------------- /angular-chat/src/app/messages/message.component.cy.ts: -------------------------------------------------------------------------------- 1 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 2 | import { CommonModule } from "@angular/common"; 3 | import { MockProvider } from "ng-mocks"; 4 | import { MessageService } from "./message.service"; 5 | import { User } from "../user/user.interface"; 6 | import { MessagesComponent } from "./messages.component"; 7 | import { Message } from "./message.interface"; 8 | import { Subject } from "rxjs"; 9 | import { MountResponse } from "cypress/angular"; 10 | 11 | const user: User = { 12 | name: "michael", 13 | id: "user_1" 14 | }; 15 | 16 | const messages: Message[] = [ 17 | { 18 | id: "id_1", 19 | text: "text_1", 20 | userId: user.id, 21 | userName: user.name, 22 | date: new Date(2023, 1, 2, 15, 5, 5) 23 | }, 24 | { 25 | id: "id_2", 26 | text: "text_2", 27 | userId: "userId_2", 28 | userName: "userName_2", 29 | date: new Date(2023, 1, 3, 15, 5, 5) 30 | } 31 | ]; 32 | let websocketMessage: Subject; 33 | const newMessage = { 34 | id: "id_3", 35 | text: "text_3", 36 | userId: "userId_3", 37 | userName: "userName_3", 38 | date: new Date(50050000) 39 | }; 40 | 41 | const imports = [CommonModule, FormsModule, ReactiveFormsModule]; 42 | const providers = [MockProvider(MessageService, { 43 | getMessages: () => Promise.resolve(messages), 44 | send: () => Promise.resolve(null) 45 | })]; 46 | const config = { 47 | providers, 48 | imports, 49 | declarations: [], 50 | componentProperties: { 51 | user: user, 52 | initWebsocketClient: (brokerURL: string, callBack: (body: string) => void) => { 53 | websocketMessage.subscribe(() => callBack(JSON.stringify(newMessage))); 54 | return { 55 | deactivate: () => Promise.resolve(), 56 | activate: () => { 57 | } 58 | }; 59 | } 60 | } 61 | }; 62 | 63 | describe("Message component", () => { 64 | beforeEach(() => { 65 | websocketMessage = new Subject(); 66 | cy.mount(MessagesComponent, config).then((wrapper) => { 67 | return cy.wrap(wrapper).as("angular"); 68 | }); 69 | }); 70 | 71 | it("display the messages given by the service", () => { 72 | const actualMessage = []; 73 | cy.get(".messages .message").each(($messageElement) => { 74 | const userName = $messageElement[0].querySelector(".user-name")?.textContent.trim(); 75 | const date = $messageElement[0].querySelector(".message-date").textContent.trim(); 76 | const text: string = $messageElement[0].querySelector(".text").textContent.trim(); 77 | actualMessage.push({ userName, date, text }); 78 | }).then(() => { 79 | expect(actualMessage).to.deep.equal([ 80 | { 81 | date: "02 Feb 2023", 82 | text: messages[0].text, 83 | userName: undefined 84 | }, 85 | { 86 | userName: messages[1].userName, 87 | date: "03 Feb 2023", 88 | text: messages[1].text 89 | } 90 | ]); 91 | }); 92 | }); 93 | 94 | it("Update the messages with the upcoming messages", () => { 95 | cy.get>("@angular").then(wrap => { 96 | websocketMessage.next("event"); 97 | return wrap.fixture.whenStable(); 98 | }); 99 | cy.get(".messages .message").last().then(($messageElement) => { 100 | const userName = $messageElement[0].querySelector(".user-name")?.textContent.trim(); 101 | const text: string = $messageElement[0].querySelector(".text").textContent.trim(); 102 | expect(userName).to.equal(newMessage.userName); 103 | expect(text).to.equal(newMessage.text); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /angular-chat/src/app/messages/message.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | id: string; 3 | text: string; 4 | userId: string; 5 | userName: string; 6 | date: Date; 7 | } 8 | -------------------------------------------------------------------------------- /angular-chat/src/app/messages/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { HttpClient } from "@angular/common/http"; 3 | import { Message } from "./message.interface"; 4 | import { map } from "rxjs/operators"; 5 | import { environment } from "../../environments/environment"; 6 | import { lastValueFrom } from "rxjs"; 7 | 8 | @Injectable({ 9 | providedIn: "root" 10 | }) 11 | export class MessageService { 12 | 13 | private server = environment.server; 14 | 15 | constructor(private http: HttpClient) { 16 | } 17 | 18 | getMessages(): Promise { 19 | return lastValueFrom(this.http.get(`${this.server}/messages`) 20 | .pipe( 21 | map((messages: Message[]) => messages.map(message => ({ ...message, date: new Date(message.date) }))) 22 | )); 23 | } 24 | 25 | send(message: { text: string; userId: string }): Promise { 26 | return lastValueFrom(this.http.post(`${this.server}/messages/new`, message)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /angular-chat/src/app/messages/messages.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | {{message.userName}} 9 | 10 | 11 | {{message.text}} 12 | 13 | {{message.date | date:'hh:mm'}} 14 | 15 | 16 | 17 | {{message.date | date:'dd MMM yyyy'}} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | send 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /angular-chat/src/app/messages/messages.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | width: 100%; 4 | height: 70vh; 5 | 6 | .message-component { 7 | height: 100%; 8 | width: 100%; 9 | 10 | .message-container { 11 | height: calc(100% - 75px); 12 | overflow-y: scroll; 13 | 14 | .messages { 15 | padding: 20px; 16 | 17 | .current-user { 18 | display: flex; 19 | justify-content: flex-end; 20 | align-items: center; 21 | } 22 | 23 | .other-user { 24 | display: flex; 25 | justify-content: flex-start; 26 | align-items: center; 27 | } 28 | 29 | .bubble { 30 | box-shadow: -1px 1px 0px #ccd6d8; 31 | padding: 10px; 32 | border: solid 1px #9f9f9f; 33 | border-radius: 10px; 34 | } 35 | 36 | .current-user, .other-user { 37 | margin-bottom: 20px; 38 | } 39 | 40 | .user-name, .message-date { 41 | color: #949494; 42 | font-size: 12px; 43 | } 44 | 45 | .user-name, .message-text { 46 | margin-bottom: 5px; 47 | } 48 | 49 | .message-text { 50 | font-size: 16px; 51 | display: flex; 52 | flex-direction: row; 53 | align-items: flex-end; 54 | align-content: flex-end; 55 | 56 | .margin-right-10 { 57 | margin-right: 10px; 58 | } 59 | } 60 | } 61 | } 62 | 63 | .message-form { 64 | width: 100%; 65 | background-color: #EEEEEE; 66 | height: 75px; 67 | 68 | display: flex; 69 | flex-direction: row; 70 | justify-content: flex-start; 71 | align-items: center; 72 | align-content: center; 73 | 74 | input { 75 | font-size: 16px; 76 | padding: 15px 25px; 77 | width: calc(100% - 100px); 78 | height: 40px; 79 | margin-left: 30px; 80 | margin-right: 30px; 81 | border-radius: 25px; 82 | background-color: #ffffff; 83 | outline: none; 84 | border: none; 85 | box-sizing: border-box; 86 | } 87 | 88 | .send-button { 89 | font-size: 20px; 90 | width: 100px; 91 | 92 | button { 93 | cursor: pointer; 94 | outline: none; 95 | border: none; 96 | background: none; 97 | } 98 | } 99 | } 100 | } 101 | 102 | ::-webkit-scrollbar { 103 | width: 8px; 104 | } 105 | 106 | ::-webkit-scrollbar-button { 107 | width: 8px; 108 | height: 5px; 109 | } 110 | 111 | ::-webkit-scrollbar-track { 112 | background: #eee; 113 | border: thin solid lightgray; 114 | box-shadow: 0 0 3px #dfdfdf inset; 115 | border-radius: 10px; 116 | } 117 | 118 | ::-webkit-scrollbar-thumb { 119 | background: #999; 120 | border: thin solid gray; 121 | border-radius: 10px; 122 | } 123 | 124 | ::-webkit-scrollbar-thumb:hover { 125 | background: #7d7d7d; 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /angular-chat/src/app/messages/messages.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from "@angular/core"; 2 | import { FormControl } from "@angular/forms"; 3 | import { MessageService } from "./message.service"; 4 | import { Message } from "./message.interface"; 5 | import { User } from "../user/user.interface"; 6 | import { environment } from "../../environments/environment"; 7 | import { Client } from "@stomp/stompjs"; 8 | 9 | @Component({ 10 | selector: "app-messages", 11 | templateUrl: "./messages.component.html", 12 | styleUrls: ["./messages.component.scss"] 13 | }) 14 | export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit { 15 | 16 | @Input() 17 | user: User; 18 | 19 | @ViewChild("messageInput") 20 | messageInput: ElementRef; 21 | 22 | messages: Message[] = []; 23 | name = new FormControl(""); 24 | websocketClient: Client; 25 | 26 | constructor(private messageService: MessageService) { 27 | } 28 | 29 | ngOnInit(): void { 30 | this.connect(); 31 | this.messageService.getMessages().then((messages: Message[]) => { 32 | this.messages = messages; 33 | }); 34 | } 35 | 36 | ngAfterViewInit(): void { 37 | setTimeout(() => this.messageInput.nativeElement.focus()); 38 | } 39 | 40 | ngOnDestroy(): void { 41 | this.disconnect(); 42 | } 43 | 44 | connect(): void { 45 | this.websocketClient = this.initWebsocketClient(environment.webSocket, (body: string) => { 46 | const message: Message = JSON.parse(body); 47 | this.messages.push({ ...message, date: new Date(message.date) }); 48 | }); 49 | this.websocketClient.activate(); 50 | } 51 | 52 | private initWebsocketClient(brokerURL: string, callBack: (body: string) => void): Client { 53 | return new Client({ 54 | brokerURL: brokerURL, 55 | onConnect: () => { 56 | this.websocketClient.subscribe("/chat", (frame: { body: string }) => callBack(frame.body)); 57 | } 58 | }); 59 | } 60 | 61 | disconnect(): void { 62 | if (!!this.websocketClient) { 63 | this.websocketClient.deactivate().then(() => console.warn("Disconnected")); 64 | } 65 | } 66 | 67 | send(): void { 68 | this.messageService.send({ 69 | text: this.name.value, 70 | userId: this.user.id 71 | }).then(() => this.name.setValue("")); 72 | } 73 | 74 | isToday(date: Date): boolean { 75 | const today = new Date(); 76 | return date.getDate() == today.getDate() 77 | && date.getMonth() == today.getMonth() 78 | && date.getFullYear() == today.getFullYear(); 79 | } 80 | 81 | isCurrentUser(userId: string): boolean { 82 | return userId === this.user.id; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /angular-chat/src/app/user/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | name: string; 3 | id: string; 4 | } 5 | -------------------------------------------------------------------------------- /angular-chat/src/app/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | import {User} from './user.interface'; 4 | import {environment} from '../../environments/environment'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class UserService { 10 | 11 | private server = environment.server; 12 | 13 | constructor(private http: HttpClient) { 14 | } 15 | 16 | getUser(name: string): Promise { 17 | return this.http.post(`${this.server}/users`, {name}).toPromise(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /angular-chat/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wandri/chat-spring-kafka-angular-react-vue/58eb6d42b6e7b9d93e40f3a8f97b3223f618accc/angular-chat/src/assets/.gitkeep -------------------------------------------------------------------------------- /angular-chat/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /angular-chat/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | server: 'http://localhost:8000', 8 | webSocket: 'ws://localhost:8000/socket' 9 | }; 10 | 11 | /* 12 | * For easier debugging in development mode, you can import the following file 13 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 14 | * 15 | * This import should be commented out in production mode because it will have a negative impact 16 | * on performance if an error is thrown. 17 | */ 18 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 19 | -------------------------------------------------------------------------------- /angular-chat/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wandri/chat-spring-kafka-angular-react-vue/58eb6d42b6e7b9d93e40f3a8f97b3223f618accc/angular-chat/src/favicon.ico -------------------------------------------------------------------------------- /angular-chat/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularChats 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /angular-chat/src/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core'; 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 3 | 4 | import {AppModule} from '../../angular-chat/src/app/app.module'; 5 | import {environment} from '../../angular-chat/src/environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /angular-chat/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /angular-chat/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import url('https://fonts.googleapis.com/css?family=Roboto'); 3 | @import url('https://fonts.googleapis.com/icon?family=Material+Icons'); 4 | 5 | html, body { 6 | height: 100%; 7 | margin: 0; 8 | } 9 | 10 | body { 11 | margin: 0; 12 | font-family: Roboto, "Helvetica Neue", sans-serif; 13 | background-color: #D9DBD6; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /angular-chat/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "../angular-chat/src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /angular-chat/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "ES2022", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ], 19 | "useDefineForClassFields": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /angular-chat/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /angular-chat/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "directive-selector": [ 140 | true, 141 | "attribute", 142 | "app", 143 | "camelCase" 144 | ], 145 | "component-selector": [ 146 | true, 147 | "element", 148 | "app", 149 | "kebab-case" 150 | ] 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /chat-api/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | /target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | /nbproject/private/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ 26 | /build/ 27 | 28 | ### VS Code ### 29 | .vscode/ 30 | -------------------------------------------------------------------------------- /chat-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.0.6 9 | 10 | 11 | com.wandrilleCorp 12 | chat-api 13 | 2 14 | chat-api 15 | Demo project for Spring Boot 16 | 17 | 18 | 17 19 | 2022.0.2 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-mongodb 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-websocket 30 | 31 | 32 | org.springframework.cloud 33 | spring-cloud-stream 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-stream-binder-kafka 38 | 39 | 40 | org.springframework.kafka 41 | spring-kafka 42 | 43 | 44 | org.apache.commons 45 | commons-lang3 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-test 50 | test 51 | 52 | 53 | org.springframework.cloud 54 | spring-cloud-stream-test-binder 55 | test 56 | 57 | 58 | org.springframework.kafka 59 | spring-kafka-test 60 | test 61 | 62 | 63 | io.confluent 64 | kafka-avro-serializer 65 | 7.3.3 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.springframework.cloud 73 | spring-cloud-dependencies 74 | ${spring-cloud.version} 75 | pom 76 | import 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | 87 | 88 | 89 | org.apache.avro 90 | avro-maven-plugin 91 | 1.11.1 92 | 93 | String 94 | 95 | 96 | 97 | generate-sources 98 | 99 | schema 100 | 101 | 102 | ${project.basedir}/src/main/resources/avro/ 103 | ${project.basedir}/target/generated-sources/avro 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | confluent 114 | https://packages.confluent.io/maven/ 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/ChatApiApplication.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ChatApiApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ChatApiApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/api/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.api; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void registerStompEndpoints(StompEndpointRegistry registry) { 15 | registry.addEndpoint("/socket") 16 | .setAllowedOrigins("http://localhost:4200", "http://localhost:5173", "http://localhost:3000"); 17 | } 18 | 19 | @Override 20 | public void configureMessageBroker(MessageBrokerRegistry registry) { 21 | registry.setApplicationDestinationPrefixes("/app"); 22 | registry.enableSimpleBroker("/chat"); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/api/message/MessageController.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.api.message; 2 | 3 | import com.wandrillecorp.chatapi.application.MessageManager; 4 | import com.wandrillecorp.chatapi.domain.message.Message; 5 | import jakarta.validation.Valid; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import java.util.List; 10 | 11 | @CrossOrigin(origins = {"http://localhost:4200", "http://localhost:5173", "http://localhost:3000"}, maxAge = 3600) 12 | @RestController 13 | @RequestMapping("/messages") 14 | public class MessageController { 15 | private final MessageManager messageManager; 16 | 17 | @Autowired 18 | public MessageController( 19 | MessageManager messageManager) { 20 | this.messageManager = messageManager; 21 | } 22 | 23 | @GetMapping() 24 | public List getMessages() throws Exception { 25 | return messageManager.getAllMessages(); 26 | } 27 | 28 | @PostMapping("/new") 29 | public void addNewMessage( 30 | @Valid @RequestBody MessageCreatedCommand command 31 | ) { 32 | try { 33 | Message message = messageManager.save(command.getUserId(), command.getText()); 34 | messageManager.send(message); 35 | } catch (Exception e) { 36 | throw new RuntimeException(e); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/api/message/MessageCreatedCommand.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.api.message; 2 | 3 | 4 | import jakarta.validation.constraints.NotNull; 5 | 6 | public class MessageCreatedCommand { 7 | @NotNull 8 | private String text; 9 | @NotNull 10 | private String userId; 11 | 12 | public MessageCreatedCommand(String text, String userId) { 13 | this.text = text; 14 | this.userId = userId; 15 | } 16 | 17 | public String getText() { 18 | return text; 19 | } 20 | 21 | public String getUserId() { 22 | return userId; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/api/message/WebSocketMessage.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.api.message; 2 | 3 | public class WebSocketMessage { 4 | } 5 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/api/user/UserController.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.api.user; 2 | 3 | import com.wandrillecorp.chatapi.application.UserManager; 4 | import com.wandrillecorp.chatapi.domain.user.User; 5 | import jakarta.validation.Valid; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.*; 9 | import org.springframework.web.server.ResponseStatusException; 10 | 11 | 12 | @CrossOrigin(origins = {"http://localhost:4200", "http://localhost:5173", "http://localhost:3000"}, maxAge = 3600) 13 | @RestController 14 | @RequestMapping("/users") 15 | public class UserController { 16 | private final UserManager userManager; 17 | 18 | @Autowired 19 | public UserController(UserManager userManager) { 20 | this.userManager = userManager; 21 | } 22 | 23 | @PostMapping() 24 | public User createUser( 25 | @Valid @RequestBody UserCreatedCommand command 26 | ) { 27 | try { 28 | return this.userManager.findOrCreateUser(command.getName()); 29 | } catch (Exception e) { 30 | throw new ResponseStatusException(HttpStatus.NOT_FOUND, "An error has occurred, sorry."); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/api/user/UserCreatedCommand.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.api.user; 2 | 3 | 4 | import jakarta.validation.constraints.NotNull; 5 | 6 | public class UserCreatedCommand { 7 | @NotNull 8 | private String name; 9 | 10 | public UserCreatedCommand() { 11 | } 12 | 13 | public UserCreatedCommand(String name) { 14 | this.name = name; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/application/MessageManager.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.application; 2 | 3 | import com.wandrillecorp.chatapi.domain.bus.ChatMessageBus; 4 | import com.wandrillecorp.chatapi.domain.message.Message; 5 | import com.wandrillecorp.chatapi.domain.message.MessageRepository; 6 | import com.wandrillecorp.chatapi.domain.user.User; 7 | import com.wandrillecorp.chatapi.domain.user.UserRepository; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.sql.Date; 11 | import java.time.Instant; 12 | import java.util.List; 13 | 14 | @Service 15 | public class MessageManager { 16 | private MessageRepository messageRepository; 17 | private UserRepository userRepository; 18 | private ChatMessageBus chatMessageBus; 19 | 20 | public MessageManager( 21 | MessageRepository messageRepository, 22 | UserRepository userRepository, 23 | ChatMessageBus chatMessageBus 24 | ) { 25 | this.messageRepository = messageRepository; 26 | this.userRepository = userRepository; 27 | this.chatMessageBus = chatMessageBus; 28 | } 29 | 30 | public List getAllMessages() { 31 | return messageRepository.findAllOrderByDate(); 32 | } 33 | 34 | public Message save(String userId, String text) { 35 | User user = userRepository.find(userId); 36 | if (user != null) { 37 | return messageRepository.save(new Message(user.getName(), userId, text, Date.from(Instant.now()))); 38 | } else { 39 | throw new IllegalStateException(); 40 | } 41 | } 42 | 43 | public void send(Message message) { 44 | chatMessageBus.emit(message); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/application/UserManager.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.application; 2 | 3 | import com.wandrillecorp.chatapi.domain.user.User; 4 | import com.wandrillecorp.chatapi.domain.user.UserRepository; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | public class UserManager { 9 | 10 | private UserRepository userRepository; 11 | 12 | public UserManager(UserRepository userRepository) { 13 | this.userRepository = userRepository; 14 | } 15 | 16 | public User findOrCreateUser(String userName) { 17 | User existingUser = userRepository.findByName(userName); 18 | if (existingUser != null) { 19 | return existingUser; 20 | } else { 21 | return userRepository.save(userName); 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/domain/ValueObject.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.domain; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | public class ValueObject { 8 | 9 | @Override 10 | public int hashCode() { 11 | return HashCodeBuilder.reflectionHashCode(this); 12 | } 13 | 14 | @Override 15 | public boolean equals(Object o) { 16 | return EqualsBuilder.reflectionEquals(this, o); 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return ToStringBuilder.reflectionToString(this); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/domain/bus/ChatMessageBus.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.domain.bus; 2 | 3 | import com.wandrillecorp.chatapi.domain.message.Message; 4 | 5 | public interface ChatMessageBus { 6 | void emit(Message message); 7 | } 8 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/domain/message/Message.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.domain.message; 2 | 3 | import com.wandrillecorp.chatapi.domain.ValueObject; 4 | 5 | import java.io.Serializable; 6 | import java.util.Date; 7 | 8 | public class Message extends ValueObject implements Serializable { 9 | private String id; 10 | private String userName; 11 | private String userId; 12 | private String text; 13 | private Date date; 14 | 15 | public Message() { 16 | } 17 | 18 | public Message(String id, String userName, String userId, String text, Date date) { 19 | this.id = id; 20 | this.userName = userName; 21 | this.userId = userId; 22 | this.text = text; 23 | this.date = date; 24 | } 25 | 26 | public Message(String userName, String userId, String text, Date date) { 27 | this(null, userName, userId, text, date); 28 | } 29 | 30 | public String getUserName() { 31 | return userName; 32 | } 33 | 34 | public String getUserId() { 35 | return userId; 36 | } 37 | 38 | public String getText() { 39 | return text; 40 | } 41 | 42 | public Date getDate() { 43 | return date; 44 | } 45 | 46 | public String getId() { 47 | return id; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/domain/message/MessageRepository.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.domain.message; 2 | 3 | import org.springframework.stereotype.Repository; 4 | 5 | import java.util.List; 6 | 7 | @Repository 8 | public interface MessageRepository { 9 | Message save(Message message); 10 | 11 | List findAllOrderByDate(); 12 | } 13 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/domain/user/User.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.domain.user; 2 | 3 | import java.io.Serializable; 4 | 5 | public class User implements Serializable { 6 | private String id; 7 | private String name; 8 | 9 | public User(String id, String name) { 10 | this.id = id; 11 | this.name = name; 12 | } 13 | 14 | public String getName() { 15 | return name; 16 | } 17 | 18 | public String getId() { 19 | return id; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/domain/user/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.domain.user; 2 | 3 | import org.springframework.stereotype.Repository; 4 | 5 | @Repository 6 | public interface UserRepository { 7 | User findByName(String userName); 8 | 9 | User find(String id); 10 | 11 | User save(String userName); 12 | } 13 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/infrastructure/messageBus/KafkaEmission.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.infrastructure.messageBus; 2 | 3 | import com.wandrillecorp.chatapi.domain.bus.ChatMessageBus; 4 | import com.wandrillecorp.chatapi.domain.message.Message; 5 | import com.wandrillecorp.avro.message.MessageAvro; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.kafka.core.KafkaTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class KafkaEmission implements ChatMessageBus { 12 | 13 | private final KafkaTemplate kafkaTemplate; 14 | 15 | @Autowired 16 | public KafkaEmission(KafkaTemplate kafkaTemplate) { 17 | this.kafkaTemplate = kafkaTemplate; 18 | } 19 | 20 | @Override 21 | public void emit(Message message) { 22 | MessageAvro messageAvro = new MessageAvro(message.getId(), message.getText(), message.getUserId(), 23 | message.getUserName(), message.getDate().getTime()); 24 | kafkaTemplate.send("chat", messageAvro); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/infrastructure/messageBus/KafkaEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.infrastructure.messageBus; 2 | 3 | import com.wandrillecorp.chatapi.domain.message.Message; 4 | import com.wandrillecorp.avro.message.MessageAvro; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.kafka.annotation.KafkaListener; 7 | import org.springframework.messaging.simp.SimpMessagingTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Date; 11 | 12 | @Component 13 | public class KafkaEventHandler { 14 | 15 | private SimpMessagingTemplate template; 16 | 17 | @Autowired 18 | public KafkaEventHandler(SimpMessagingTemplate template) { 19 | this.template = template; 20 | } 21 | 22 | @KafkaListener(topics = "chat") 23 | public void listen(MessageAvro messageAvro) { 24 | Message message = new Message(messageAvro.getId(), messageAvro.getUserName(), 25 | messageAvro.getUserId(), messageAvro.getText(), new Date(Long.valueOf(messageAvro.getDate()))); 26 | template.convertAndSend("/chat", message); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/infrastructure/repository/message/DefaultMongoMessageRepository.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.infrastructure.repository.message; 2 | 3 | import com.wandrillecorp.chatapi.domain.message.Message; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface DefaultMongoMessageRepository extends MongoRepository { 9 | } 10 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/infrastructure/repository/message/MongoMessageRepository.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.infrastructure.repository.message; 2 | 3 | import com.wandrillecorp.chatapi.domain.message.Message; 4 | import com.wandrillecorp.chatapi.domain.message.MessageRepository; 5 | import org.springframework.data.domain.Sort; 6 | import org.springframework.data.mongodb.core.MongoOperations; 7 | import org.springframework.data.mongodb.core.query.Query; 8 | import org.springframework.stereotype.Repository; 9 | 10 | import java.util.List; 11 | 12 | import static org.springframework.data.domain.Sort.Direction.ASC; 13 | 14 | @Repository 15 | public class MongoMessageRepository implements MessageRepository { 16 | 17 | private DefaultMongoMessageRepository defaultMongMessageRepository; 18 | private MongoOperations mongoOperations; 19 | 20 | public MongoMessageRepository( 21 | DefaultMongoMessageRepository defaultMongMessageRepository, 22 | MongoOperations mongoOperations 23 | ) { 24 | this.defaultMongMessageRepository = defaultMongMessageRepository; 25 | this.mongoOperations = mongoOperations; 26 | } 27 | 28 | @Override 29 | public Message save(Message message) { 30 | return defaultMongMessageRepository.save(message); 31 | } 32 | 33 | @Override 34 | public List findAllOrderByDate() { 35 | Query query = new Query(); 36 | query.with(Sort.by(ASC, "date")); 37 | return mongoOperations.find(query, Message.class); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/infrastructure/repository/user/DefaultMongoUserRepository.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.infrastructure.repository.user; 2 | 3 | import com.wandrillecorp.chatapi.domain.user.User; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface DefaultMongoUserRepository extends MongoRepository { 11 | User findByName(String name); 12 | 13 | Optional findById(String id); 14 | } 15 | -------------------------------------------------------------------------------- /chat-api/src/main/java/com/wandrillecorp/chatapi/infrastructure/repository/user/MongoUserRepository.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi.infrastructure.repository.user; 2 | 3 | import com.wandrillecorp.chatapi.domain.user.User; 4 | import com.wandrillecorp.chatapi.domain.user.UserRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public class MongoUserRepository implements UserRepository { 9 | 10 | private DefaultMongoUserRepository defaultMongoUserRepository; 11 | 12 | public MongoUserRepository(DefaultMongoUserRepository defaultMongoUserRepository) { 13 | this.defaultMongoUserRepository = defaultMongoUserRepository; 14 | } 15 | 16 | @Override 17 | public User findByName(String userName) { 18 | return defaultMongoUserRepository.findByName(userName); 19 | } 20 | 21 | @Override 22 | public User find(String id) { 23 | return defaultMongoUserRepository.findById(id).orElse(null); 24 | } 25 | 26 | @Override 27 | public User save(String userName) { 28 | return defaultMongoUserRepository.save(new User(null, userName)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /chat-api/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data: 3 | mongodb: 4 | host: localhost 5 | port: 27017 6 | kafka: 7 | bootstrap-servers: "http://localhost:9092" 8 | producer: 9 | key-serializer: org.apache.kafka.common.serialization.StringSerializer 10 | value-serializer: io.confluent.kafka.serializers.KafkaAvroSerializer 11 | consumer: 12 | group-id: "chat-api" 13 | auto-offset-reset: earliest 14 | key-deserializer: org.apache.kafka.common.serialization.StringDeserializer 15 | value-deserializer: io.confluent.kafka.serializers.KafkaAvroDeserializer 16 | enable-auto-commit: true 17 | properties: 18 | specific.avro.reader: true 19 | schema.registry.url: "http://localhost:8085" 20 | server: 21 | port: 8000 22 | 23 | logging: 24 | level: 25 | org.apache.kafka.clients: WARN 26 | io.confluent.kafka: WARN 27 | -------------------------------------------------------------------------------- /chat-api/src/main/resources/avro/message.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "MessageAvro", 4 | "namespace": "com.wandrillecorp.avro.message", 5 | "doc": "A view over all messages", 6 | "fields": [ 7 | { 8 | "name": "id", 9 | "type": "string" 10 | }, 11 | { 12 | "name": "text", 13 | "type": "string" 14 | }, 15 | { 16 | "name": "userId", 17 | "type": "string" 18 | }, 19 | { 20 | "name": "userName", 21 | "type": "string" 22 | }, 23 | { 24 | "name": "date", 25 | "type": "long", 26 | "logicalType": "timestamp-millis" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /chat-api/src/test/java/com/wandrillecorp/chatapi/ChatApiApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.wandrillecorp.chatapi; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | public class ChatApiApplicationTests { 8 | 9 | @Test 10 | public void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /deploy/deploy.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /deploy/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | database: 5 | container_name: MondoDB 6 | image: mongo:6.0.1 7 | ports: 8 | - "27017:27017" 9 | 10 | zookeeper: 11 | image: confluentinc/cp-zookeeper:7.3.2 12 | container_name: zookeeper 13 | environment: 14 | ZOOKEEPER_CLIENT_PORT: 2181 15 | ZOOKEEPER_TICK_TIME: 2000 16 | 17 | broker: 18 | image: confluentinc/cp-server:7.3.2 19 | container_name: broker 20 | ports: 21 | # To learn about configuring Kafka for access across networks see 22 | # https://www.confluent.io/blog/kafka-client-cannot-connect-to-broker-on-aws-on-docker-etc/ 23 | - "9092:9092" 24 | depends_on: 25 | - zookeeper 26 | environment: 27 | KAFKA_BROKER_ID: 1 28 | KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' 29 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT 30 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092 31 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 32 | KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 33 | KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 34 | KAFKA_SCHEMA_REGISTRY_URL: "schema-registry:8085" 35 | 36 | schema-registry: 37 | image: confluentinc/cp-schema-registry:7.3.2 38 | depends_on: 39 | - broker 40 | environment: 41 | SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: "broker:29092" 42 | SCHEMA_REGISTRY_HOST_NAME: schema-registry 43 | SCHEMA_REGISTRY_LISTENERS: "http://0.0.0.0:8085" 44 | ports: 45 | - "8085:8085" 46 | 47 | kafka-init: 48 | image: confluentinc/cp-kafka:7.3.2 49 | command: | 50 | bash -c " 51 | until kafka-topics --create --if-not-exists --bootstrap-server broker:29092 --topic chat; do sleep 1; done 52 | " 53 | environment: 54 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 55 | depends_on: 56 | - broker 57 | -------------------------------------------------------------------------------- /doc/message_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wandri/chat-spring-kafka-angular-react-vue/58eb6d42b6e7b9d93e40f3a8f97b3223f618accc/doc/message_screen.jpg -------------------------------------------------------------------------------- /doc/user_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wandri/chat-spring-kafka-angular-react-vue/58eb6d42b6e7b9d93e40f3a8f97b3223f618accc/doc/user_screen.jpg -------------------------------------------------------------------------------- /e2e-tests/.cypress-cucumber-preprocessorrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "json": { 3 | "enabled": true, 4 | "output": "cucumber-report.json" 5 | }, 6 | "html": { 7 | "enabled": true 8 | }, 9 | "stepDefinitions": [ 10 | "cypress/e2e/[filepath].ts", 11 | "cypress/e2e/common-step-definitions/*.ts" 12 | ], 13 | "omitFiltered": true, 14 | "filterSpecs": true 15 | } 16 | -------------------------------------------------------------------------------- /e2e-tests/.gitignore: -------------------------------------------------------------------------------- 1 | cucumber-messages.ndjson 2 | cucumber-report.html 3 | cucumber-report.json 4 | cypress/screenshots 5 | cypress/videos 6 | node_modules 7 | -------------------------------------------------------------------------------- /e2e-tests/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | import { addCucumberPreprocessorPlugin } from "@badeball/cypress-cucumber-preprocessor"; 3 | import createEsbuildPlugin from "@badeball/cypress-cucumber-preprocessor/esbuild"; 4 | import createBundler from "@bahmutov/cypress-esbuild-preprocessor"; 5 | 6 | export default defineConfig({ 7 | e2e: { 8 | specPattern: "**/*.feature", 9 | async setupNodeEvents( 10 | on: Cypress.PluginEvents, 11 | config: Cypress.PluginConfigOptions 12 | ): Promise { 13 | // This is required for the preprocessor to be able to generate JSON reports after each run, and more, 14 | await addCucumberPreprocessorPlugin(on, config); 15 | 16 | on( 17 | "file:preprocessor", 18 | createBundler({ 19 | plugins: [createEsbuildPlugin(config)], 20 | }) 21 | ); 22 | 23 | // Make sure to return the config object as it might have been modified by the plugin. 24 | return config; 25 | }, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /e2e-tests/cypress/e2e/chat-access.feature: -------------------------------------------------------------------------------- 1 | Feature: Access to the chat 2 | 3 | Scenario: I can login to the Angular chat and see the chat 4 | When I login into the Angular chat with the name "Mark" 5 | Then I see the chat 6 | 7 | Scenario: I can login to the React chat and see the chat 8 | When I login into the React chat with the name "Tom" 9 | Then I see the chat 10 | 11 | Scenario: I can login to the Vue chat and see the chat 12 | When I login into the Vue chat with the name "Peter" 13 | Then I see the chat 14 | -------------------------------------------------------------------------------- /e2e-tests/cypress/e2e/chat-access.ts: -------------------------------------------------------------------------------- 1 | import { Then } from '@badeball/cypress-cucumber-preprocessor'; 2 | 3 | Then("I see the chat", ()=> { 4 | cy.get('[data-cy="message-component"]').should('exist'); 5 | }); 6 | -------------------------------------------------------------------------------- /e2e-tests/cypress/e2e/common-step-definitions/common.ts: -------------------------------------------------------------------------------- 1 | import { When } from '@badeball/cypress-cucumber-preprocessor'; 2 | import { System, systemUrl } from './system'; 3 | 4 | When(/^I login into the (Angular|React|Vue) chat with the name "([^"]*)"$/, (system: System, name: string) => { 5 | cy.log(system); 6 | let url: string = systemUrl[system]; 7 | cy.visit(url); 8 | 9 | cy.get('[data-cy="application-component"]').should('exist').contains('Welcome to the chat'); 10 | cy.get('[data-cy="user-form"] input').type(name); 11 | cy.get('button[type="submit"]').click(); 12 | }); 13 | -------------------------------------------------------------------------------- /e2e-tests/cypress/e2e/common-step-definitions/system.ts: -------------------------------------------------------------------------------- 1 | export const enum System { 2 | "Angular" = "Angular", "React" = "React", "Vue" = "Vue" 3 | } 4 | 5 | export const systemUrl = { 6 | "Angular": "http://localhost:4200", 7 | "Vue": "http://localhost:5173", 8 | "React": "http://localhost:3000" 9 | }; 10 | -------------------------------------------------------------------------------- /e2e-tests/cypress/e2e/messages.feature: -------------------------------------------------------------------------------- 1 | Feature: Message writing 2 | 3 | Scenario: I can write and read messages in the Angular chat 4 | When I login into the Angular chat with the name "Mark" 5 | When I send the message "Welcome, I'm Mark" 6 | Then The last message is "Welcome, I'm Mark" 7 | 8 | Scenario: I can write and read messages in the React chat 9 | When I login into the React chat with the name "Tom" 10 | When I send the message "Welcome, I'm Tom" 11 | Then The last message is "Welcome, I'm Tom" 12 | 13 | Scenario: I can write and read messages in the Vue chat 14 | When I login into the Vue chat with the name "Peter" 15 | When I send the message "Welcome, I'm Peter" 16 | Then The last message is "Welcome, I'm Peter" 17 | -------------------------------------------------------------------------------- /e2e-tests/cypress/e2e/messages.ts: -------------------------------------------------------------------------------- 1 | import { Then, When } from '@badeball/cypress-cucumber-preprocessor'; 2 | 3 | When('I send the message {string}', (message: string) => { 4 | cy.get('[data-cy="message-form"] input').type(message); 5 | cy.get('[data-cy="message-form"] button').click(); 6 | }); 7 | 8 | Then('The last message is {string}', (message: string) => { 9 | cy.get('[data-cy="message"]').last().should('contain.text', message); 10 | }); 11 | -------------------------------------------------------------------------------- /e2e-tests/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /e2e-tests/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /e2e-tests/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /e2e-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress", 3 | "version": "1.0.0", 4 | "description": "tests", 5 | "scripts": { 6 | "open": "npx cypress open --e2e", 7 | "start": "npx cypress run --e2e" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "cypress": "^12.11.0", 13 | "typescript": "^5.0.4", 14 | "@badeball/cypress-cucumber-preprocessor": "^17.0.0", 15 | "@bahmutov/cypress-esbuild-preprocessor": "^2.2.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 4 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 5 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 6 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 7 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 8 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 9 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 10 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 11 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 12 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 13 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 14 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 15 | 16 | /* Modules */ 17 | "module": "commonjs", /* Specify what module code is generated. */ 18 | // "rootDir": "./", /* Specify the root folder within your source files. */ 19 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 20 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 21 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 22 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 23 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 24 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 25 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 26 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 27 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 28 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 29 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 30 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 31 | // "resolveJsonModule": true, /* Enable importing .json files. */ 32 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 33 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 34 | 35 | /* JavaScript Support */ 36 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 37 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 38 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 39 | 40 | /* Emit */ 41 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 42 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 43 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 44 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 45 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 46 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 47 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 48 | // "removeComments": true, /* Disable emitting comments. */ 49 | // "noEmit": true, /* Disable emitting files from a compilation. */ 50 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 51 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 52 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 53 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 56 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 57 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 58 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 59 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 60 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 61 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 62 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 63 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 64 | 65 | /* Interop Constraints */ 66 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 67 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 68 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 69 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 70 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 71 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 72 | 73 | /* Type Checking */ 74 | "strict": true, /* Enable all strict type-checking options. */ 75 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 76 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 77 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 78 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 79 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 80 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 81 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 82 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 83 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 84 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 85 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 86 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 87 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 88 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 89 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 90 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 91 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 92 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 93 | 94 | /* Completeness */ 95 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 96 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /react-chat/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /react-chat/.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /react-chat/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /react-chat/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /react-chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-chat", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.10.6", 13 | "@emotion/styled": "^11.10.6", 14 | "@fontsource/roboto": "^4.5.8", 15 | "@mui/material": "^5.12.0", 16 | "@mui/icons-material": "5.11.16", 17 | "@types/node": "18.15.11", 18 | "@types/react": "18.0.35", 19 | "@types/react-dom": "18.0.11", 20 | "autoprefixer": "10.4.14", 21 | "axios": "^1.3.5", 22 | "eslint": "8.38.0", 23 | "eslint-config-next": "13.3.0", 24 | "next": "13.3.0", 25 | "postcss": "8.4.21", 26 | "react": "18.2.0", 27 | "react-dom": "18.2.0", 28 | "@stomp/stompjs": "^7.0.0", 29 | "typescript": "5.0.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /react-chat/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /react-chat/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wandri/chat-spring-kafka-angular-react-vue/58eb6d42b6e7b9d93e40f3a8f97b3223f618accc/react-chat/public/favicon.ico -------------------------------------------------------------------------------- /react-chat/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /react-chat/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /react-chat/src/interface/message.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | id: string; 3 | text: string; 4 | userId: string; 5 | userName: string; 6 | date: Date; 7 | } 8 | -------------------------------------------------------------------------------- /react-chat/src/interface/user.interface.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | name: string; 3 | id: string; 4 | 5 | constructor(name: string, id: string) { 6 | this.name = name; 7 | this.id = id; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /react-chat/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.scss' 2 | import type { AppProps } from 'next/app' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /react-chat/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from "next/document"; 2 | import { height } from "@mui/system"; 3 | 4 | export default function Document() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /react-chat/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import axios from "axios"; 3 | import { Button } from "@mui/material"; 4 | import { User } from "@/interface/user.interface"; 5 | import Messages from "@/pages/messages/Messages"; 6 | 7 | const server = "http://localhost:8000"; 8 | 9 | export default function Home() { 10 | 11 | const [user, setUser] = useState(null); 12 | const [userName, setUserName] = useState(``); 13 | 14 | function handleUserNameChange(event: { target: { value: React.SetStateAction; }; }) { 15 | // @ts-ignore 16 | setUserName(event.target.value); 17 | } 18 | 19 | function displayUserInput() { 20 | return ( 21 | 22 | 23 | Welcome to the chat 24 | 25 | 26 | What is your name ? 27 | 28 | 29 | 31 | 32 | Let's go! 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | function displayMessages() { 40 | return ; 41 | } 42 | 43 | function activeUser(event: { preventDefault: () => void; }) { 44 | event.preventDefault(); 45 | if (userName !== ``) { 46 | return axios.post(`${server}/users`, { name: userName }).then(response => { 47 | setUser(response.data); 48 | }, 49 | (error) => console.error(error)); 50 | } 51 | } 52 | 53 | return ( 54 | 55 | 56 | {user ? displayMessages() : displayUserInput()} 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /react-chat/src/pages/messages/Messages.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import styles from "../../styles/Message.module.scss"; 3 | import axios from "axios"; 4 | import { Message } from "@/interface/message.interface"; 5 | import { User } from "@/interface/user.interface"; 6 | import { Client } from "@stomp/stompjs"; 7 | import { Button } from "@mui/material"; 8 | import SendIcon from "@mui/icons-material/Send"; 9 | 10 | export default function Messages(props: { user: User | null }) { 11 | 12 | const server = "http://localhost:8000"; 13 | const webSocket = "ws://localhost:8000/socket"; 14 | 15 | 16 | const [messages, setMessages] = useState([]); 17 | const [message, setMessage] = useState(""); 18 | 19 | 20 | const ws: Client = new Client({ 21 | brokerURL: webSocket, 22 | onConnect: () => { 23 | ws.subscribe("/chat", (frame: { body: string }) => { 24 | const message = JSON.parse(frame.body); 25 | const formattedMessage = { ...message, date: new Date(message.date) }; 26 | setMessages([...messages, formattedMessage]); 27 | }); 28 | } 29 | }); 30 | 31 | useEffect(() => { 32 | axios.get(`${server}/messages`) 33 | .then((response: { data: Message[] }) => { 34 | setMessages(response.data.map((message: any) => ({ ...message, date: new Date(message.date) }))); 35 | }).then(() => connect()); 36 | 37 | return () => { 38 | disconnect(); 39 | }; 40 | }); 41 | 42 | function connect(): void { 43 | ws.activate(); 44 | } 45 | 46 | function disconnect(): void { 47 | ws.deactivate(); 48 | } 49 | 50 | function isToday(date: Date): boolean { 51 | const today = new Date(); 52 | return date.getDate() === today.getDate() 53 | && date.getMonth() === today.getMonth() 54 | && date.getFullYear() === today.getFullYear(); 55 | } 56 | 57 | function getDateWithFormat(date: Date, isToday: boolean) { 58 | if (isToday) { 59 | return new Intl.DateTimeFormat("en-GB", { hour: "2-digit", minute: "numeric" }).format(date); 60 | } else { 61 | return new Intl.DateTimeFormat("en-GB", { year: "numeric", month: "long", day: "numeric" }).format(date); 62 | } 63 | } 64 | 65 | function sendMessage(event: { preventDefault: () => void; }) { 66 | event.preventDefault(); 67 | if (message !== "") { 68 | let body = { 69 | text: message, 70 | userId: props.user?.id 71 | }; 72 | return axios.post(`${server}/messages/new`, body) 73 | .then(() => setMessage("")); 74 | } 75 | } 76 | 77 | function handleMessageChange(event: { target: { value: React.SetStateAction; }; }) { 78 | setMessage(event.target.value); 79 | } 80 | 81 | function displayMessages() { 82 | let newMessages = messages.map(message => { 83 | return ( 84 | 86 | 87 | 88 | {message.userName} 89 | 90 | 91 | {message.text} 92 | {isToday(message.date) ? ( 93 | {getDateWithFormat(message.date, true)}) : null} 95 | 96 | {!isToday(message.date) ? ( 97 | {getDateWithFormat(message.date, false)}) : null} 99 | 100 | 101 | ); 102 | }); 103 | return {newMessages}; 104 | } 105 | 106 | function isCurrentUser(userId: string) { 107 | return userId === props.user?.id; 108 | } 109 | 110 | return ( 111 | 112 | 113 | {displayMessages()} 114 | 115 | 116 | 118 | 119 | }> 120 | send 121 | 122 | 123 | 124 | 125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /react-chat/src/styles/Message.module.scss: -------------------------------------------------------------------------------- 1 | .message_component { 2 | height: 500px; 3 | width: 100%; 4 | 5 | .message_container { 6 | padding: 20px; 7 | height: calc(100% - 75px); 8 | overflow-y: scroll; 9 | 10 | .messages { 11 | .current_user { 12 | display: flex; 13 | justify-content: flex-end; 14 | align-items: center; 15 | } 16 | 17 | .other_user { 18 | display: flex; 19 | justify-content: flex-start; 20 | align-items: center; 21 | } 22 | 23 | .bubble { 24 | box-shadow: -1px 1px 0px #ccd6d8; 25 | padding: 10px; 26 | border: solid 1px #9f9f9f; 27 | border-radius: 10px; 28 | } 29 | 30 | .current_user, .other_user { 31 | margin-bottom: 20px; 32 | } 33 | 34 | .user_name, .message_date { 35 | color: #949494; 36 | font-size: 12px; 37 | } 38 | 39 | .user_name, .message_text { 40 | margin-bottom: 5px; 41 | } 42 | 43 | .message_text { 44 | font-size: 16px; 45 | display: flex; 46 | flex-direction: row; 47 | justify-content: center; 48 | align-items: flex-end; 49 | align-content: flex-end; 50 | 51 | .margin_right_10 { 52 | margin-right: 10px; 53 | } 54 | } 55 | } 56 | } 57 | 58 | .message_form { 59 | width: 100%; 60 | background-color: #EEEEEE; 61 | height: 75px; 62 | 63 | display: flex; 64 | flex-direction: row; 65 | justify-content: flex-start; 66 | align-items: center; 67 | align-content: center; 68 | 69 | input { 70 | font-size: 16px; 71 | padding: 15px 25px; 72 | width: calc(100% - 150px); 73 | height: 40px; 74 | margin-left: 30px; 75 | margin-right: 30px; 76 | border-radius: 25px; 77 | background-color: #ffffff; 78 | outline: none; 79 | border: none; 80 | box-sizing: border-box; 81 | } 82 | 83 | .send_button { 84 | font-size: 20px; 85 | width: 100px; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /react-chat/src/styles/globals.scss: -------------------------------------------------------------------------------- 1 | @import '@fontsource/roboto/300.css'; 2 | @import '@fontsource/roboto/400.css'; 3 | @import '@fontsource/roboto/500.css'; 4 | @import '@fontsource/roboto/700.css'; 5 | 6 | html, body { 7 | height: 100%; 8 | margin: 0; 9 | } 10 | 11 | 12 | body { 13 | margin: 0; 14 | font-family: Roboto, "Helvetica Neue", sans-serif; 15 | background-color: #D9DBD6; 16 | } 17 | 18 | .application-component { 19 | min-height: 100vh; 20 | max-height: 100vh; 21 | min-width: 100vw; 22 | max-width: 100vw; 23 | display: flex; 24 | flex-direction: row; 25 | justify-content: center; 26 | align-items: center; 27 | align-content: center; 28 | 29 | &::before { 30 | content: ''; 31 | width: 100%; 32 | height: 200px; 33 | position: absolute; 34 | background-color: #3798D4; 35 | top: 0; 36 | z-index: -1; 37 | } 38 | 39 | .application-container { 40 | height: 100%; 41 | display: flex; 42 | flex-direction: row; 43 | justify-content: center; 44 | align-items: center; 45 | align-content: center; 46 | border-radius: 4px; 47 | box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 2px 5px 0 rgba(0, 0, 0, 0.2); 48 | background-color: #ffffff; 49 | font-size: 24px; 50 | 51 | @media (min-width: 1919px) { 52 | width: 60%; 53 | height: 90%; 54 | } 55 | 56 | .userForm { 57 | padding: 24px; 58 | display: flex; 59 | flex-direction: column; 60 | justify-content: center; 61 | align-items: center; 62 | align-content: center; 63 | 64 | .marginBottom15 { 65 | margin-bottom: 15px; 66 | } 67 | 68 | 69 | h1 { 70 | 71 | @media (max-width: 1200px) { 72 | font-size: 30px; 73 | } 74 | 75 | } 76 | 77 | form { 78 | display: flex; 79 | flex-direction: column; 80 | justify-content: center; 81 | align-items: center; 82 | align-content: center; 83 | 84 | input { 85 | font-size: 22px; 86 | width: 300px; 87 | border: 2px solid #aaa; 88 | border-radius: 4px; 89 | margin: 8px 0; 90 | outline: none; 91 | padding: 15px; 92 | box-sizing: border-box; 93 | transition: .3s; 94 | } 95 | 96 | input[type=text]:focus { 97 | border-color: dodgerBlue; 98 | box-shadow: 0 0 8px 0 dodgerBlue; 99 | } 100 | 101 | button { 102 | padding: 10px 20px; 103 | font-size: 22px; 104 | color: #ffffff; 105 | } 106 | 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /react-chat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /vue-chat/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier/skip-formatting' 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vue-chat/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /vue-chat/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /vue-chat/README.md: -------------------------------------------------------------------------------- 1 | # vue-chat 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | 42 | ### Run Unit Tests with [Vitest](https://vitest.dev/) 43 | 44 | ```sh 45 | npm run test:unit 46 | ``` 47 | 48 | ### Lint with [ESLint](https://eslint.org/) 49 | 50 | ```sh 51 | npm run lint 52 | ``` 53 | -------------------------------------------------------------------------------- /vue-chat/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /vue-chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /vue-chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-chat", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check build-only", 8 | "preview": "vite preview", 9 | "test:unit": "vitest", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", 12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 13 | "format": "prettier --write src/" 14 | }, 15 | "dependencies": { 16 | "@stomp/stompjs": "^7.0.0", 17 | "axios": "^1.3.6", 18 | "vue": "^3.2.47" 19 | }, 20 | "devDependencies": { 21 | "@rushstack/eslint-patch": "^1.2.0", 22 | "@types/jsdom": "^21.1.0", 23 | "@types/node": "^18.14.2", 24 | "@vitejs/plugin-vue": "^4.0.0", 25 | "@vitejs/plugin-vue-jsx": "^3.0.0", 26 | "@vue/eslint-config-prettier": "^7.1.0", 27 | "@vue/eslint-config-typescript": "^11.0.2", 28 | "@vue/test-utils": "^2.3.0", 29 | "@vue/tsconfig": "^0.1.3", 30 | "eslint": "^8.34.0", 31 | "eslint-plugin-vue": "^9.9.0", 32 | "jsdom": "^21.1.0", 33 | "npm-run-all": "^4.1.5", 34 | "prettier": "^2.8.4", 35 | "sass": "^1.62.0", 36 | "typescript": "~4.8.4", 37 | "vite": "^4.1.4", 38 | "vitest": "^0.29.1", 39 | "vue-tsc": "^1.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /vue-chat/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wandri/chat-spring-kafka-angular-react-vue/58eb6d42b6e7b9d93e40f3a8f97b3223f618accc/vue-chat/public/favicon.ico -------------------------------------------------------------------------------- /vue-chat/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue. 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /vue-chat/src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to the chat 7 | 8 | 9 | What is your name? 10 | 11 | 12 | 13 | 14 | Let's go! 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 45 | 46 | 167 | -------------------------------------------------------------------------------- /vue-chat/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wandri/chat-spring-kafka-angular-react-vue/58eb6d42b6e7b9d93e40f3a8f97b3223f618accc/vue-chat/src/assets/logo.png -------------------------------------------------------------------------------- /vue-chat/src/components/Messages.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | {{ message.userName }} 11 | 12 | 13 | {{ message.text }} 14 | 15 | {{ getDateWithFormat(message.date, true) }} 16 | 17 | 18 | 19 | {{ getDateWithFormat(message.date, false) }} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | send 30 | 31 | 32 | 33 | 34 | 35 | 36 | 114 | 115 | 237 | -------------------------------------------------------------------------------- /vue-chat/src/components/__tests__/Messages.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | 3 | import { mount } from '@vue/test-utils' 4 | import App from "../../App.vue"; 5 | 6 | describe('App', () => { 7 | it('renders properly', () => { 8 | const wrapper = mount(App) 9 | 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /vue-chat/src/components/message.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | id: string; 3 | text: string; 4 | userId: string; 5 | userName: string; 6 | date: Date; 7 | } 8 | -------------------------------------------------------------------------------- /vue-chat/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | createApp(App).mount("#app"); 5 | -------------------------------------------------------------------------------- /vue-chat/src/user.interface.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | name: string; 3 | id: string; 4 | 5 | constructor(name: string, id: string) { 6 | this.name = name; 7 | this.id = id; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /vue-chat/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /vue-chat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /vue-chat/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vue-chat/tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "lib": [], 7 | "types": ["node", "jsdom"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /vue-chat/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueJsx from '@vitejs/plugin-vue-jsx' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [vue(), vueJsx()], 10 | resolve: { 11 | alias: { 12 | '@': fileURLToPath(new URL('./src', import.meta.url)) 13 | } 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /vue-chat/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { mergeConfig } from 'vite' 3 | import { configDefaults, defineConfig } from 'vitest/config' 4 | import viteConfig from './vite.config' 5 | 6 | export default mergeConfig( 7 | viteConfig, 8 | defineConfig({ 9 | test: { 10 | environment: 'jsdom', 11 | exclude: [...configDefaults.exclude, 'e2e/*'], 12 | root: fileURLToPath(new URL('./', import.meta.url)) 13 | } 14 | }) 15 | ) 16 | --------------------------------------------------------------------------------