├── .browserslistrc ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── now.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── chat │ │ ├── chat-container │ │ │ ├── chat-container.component.html │ │ │ ├── chat-container.component.scss │ │ │ ├── chat-container.component.spec.ts │ │ │ ├── chat-container.component.ts │ │ │ └── chat-message │ │ │ │ ├── chat-message.component.html │ │ │ │ ├── chat-message.component.scss │ │ │ │ ├── chat-message.component.spec.ts │ │ │ │ └── chat-message.component.ts │ │ ├── chat-routing.module.ts │ │ ├── chat-users │ │ │ ├── chat-users.component.html │ │ │ ├── chat-users.component.scss │ │ │ ├── chat-users.component.spec.ts │ │ │ └── chat-users.component.ts │ │ ├── chat.component.html │ │ ├── chat.component.scss │ │ ├── chat.component.spec.ts │ │ ├── chat.component.ts │ │ └── chat.module.ts │ ├── core │ │ ├── models │ │ │ ├── message.ts │ │ │ └── user.ts │ │ └── services │ │ │ ├── auth-guard.service.spec.ts │ │ │ ├── auth-guard.service.ts │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth.service.ts │ │ │ ├── chat.service.spec.ts │ │ │ ├── chat.service.ts │ │ │ ├── event.service.spec.ts │ │ │ ├── event.service.ts │ │ │ ├── user.service.spec.ts │ │ │ └── user.service.ts │ ├── navbar │ │ ├── navbar.component.html │ │ ├── navbar.component.scss │ │ ├── navbar.component.spec.ts │ │ └── navbar.component.ts │ └── shared │ │ ├── loading │ │ ├── loading.component.html │ │ ├── loading.component.scss │ │ ├── loading.component.spec.ts │ │ └── loading.component.ts │ │ ├── modal │ │ ├── modal.component.html │ │ ├── modal.component.scss │ │ ├── modal.component.spec.ts │ │ └── modal.component.ts │ │ └── shared.module.ts ├── assets │ ├── .gitkeep │ ├── css │ │ └── grid.min.css │ ├── img │ │ ├── doubt.svg │ │ ├── exit.svg │ │ ├── github.svg │ │ ├── octocat-spinner-128.gif │ │ ├── shared.svg │ │ ├── smile.svg │ │ ├── upload.svg │ │ └── world.svg │ └── manifest.json ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /.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 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | package-lock.json 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 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chat for GitHub Developers 2 | 3 | [![Github Chat Logo](https://i.ibb.co/3WwBRxk/logo.png)](https://github-chat.now.sh) 4 | 5 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.1.7 6 | 7 | This is chat sample using Angular 10.1.6 and Firebase. 8 | 9 | #### Challenge 10 | 11 | - Exchange the logic to your own backend (php, nodejs, python, c#...) or using Firebase Functions (Look at the services). 12 | - Improve functionality. 13 | 14 | ## Getting started 15 | 16 | - ./src/environments/environment.ts 17 | - ./src/environments/environment.prod.ts 18 | 19 | Change this values to your personal credentials 20 | 21 | ```js 22 | const firebaseConfig = { 23 | apiKey: "{{PASTE_YOUR_API_KEY}}", 24 | authDomain: "{{PASTE_YOUR_AUTH_DOMAIN}}", 25 | databaseURL: "{{PASTE_YOUR_DATABASE_URL}}", 26 | projectId: "{{PASTE_YOUR_PROJECT_ID}}", 27 | storageBucket: "{{PASTE_YOUR_STORAGE_BUCKET}}", 28 | messagingSenderId: "{{PASTE_YOUR_MESSAGING_SENDER_ID}}", 29 | appId: "{{PASTE_YOUR_APP_ID}}", 30 | }; 31 | ``` 32 | 33 | ```sh 34 | $ npm start 35 | ``` 36 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "github-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 | "allowedCommonJsDependencies": [ 21 | "firebase", 22 | "@firebase/app", 23 | "firebase/app", 24 | "@firebase/auth", 25 | "@firebase/database" 26 | ], 27 | "outputPath": "dist", 28 | "index": "src/index.html", 29 | "main": "src/main.ts", 30 | "polyfills": "src/polyfills.ts", 31 | "tsConfig": "tsconfig.app.json", 32 | "aot": true, 33 | "assets": [ 34 | "src/favicon.ico", 35 | "src/assets", 36 | "src/assets/manifest.json" 37 | ], 38 | "styles": [ 39 | "src/styles.scss", 40 | "src/assets/css/grid.min.css", 41 | "node_modules/@ctrl/ngx-emoji-mart/picker.css" 42 | ], 43 | "scripts": [] 44 | }, 45 | "configurations": { 46 | "production": { 47 | "fileReplacements": [ 48 | { 49 | "replace": "src/environments/environment.ts", 50 | "with": "src/environments/environment.prod.ts" 51 | } 52 | ], 53 | "optimization": true, 54 | "outputHashing": "all", 55 | "sourceMap": false, 56 | "extractCss": true, 57 | "namedChunks": false, 58 | "extractLicenses": true, 59 | "vendorChunk": false, 60 | "buildOptimizer": false, 61 | "budgets": [ 62 | { 63 | "type": "initial", 64 | "maximumWarning": "5mb", 65 | "maximumError": "5mb" 66 | }, 67 | { 68 | "type": "anyComponentStyle", 69 | "maximumWarning": "6kb", 70 | "maximumError": "10kb" 71 | } 72 | ] 73 | } 74 | } 75 | }, 76 | "serve": { 77 | "builder": "@angular-devkit/build-angular:dev-server", 78 | "options": { 79 | "browserTarget": "github-chat:build" 80 | }, 81 | "configurations": { 82 | "production": { 83 | "browserTarget": "github-chat:build:production" 84 | } 85 | } 86 | }, 87 | "extract-i18n": { 88 | "builder": "@angular-devkit/build-angular:extract-i18n", 89 | "options": { 90 | "browserTarget": "github-chat:build" 91 | } 92 | }, 93 | "test": { 94 | "builder": "@angular-devkit/build-angular:karma", 95 | "options": { 96 | "main": "src/test.ts", 97 | "polyfills": "src/polyfills.ts", 98 | "tsConfig": "tsconfig.spec.json", 99 | "karmaConfig": "karma.conf.js", 100 | "assets": ["src/favicon.ico", "src/assets"], 101 | "styles": ["src/styles.scss"], 102 | "scripts": [] 103 | } 104 | }, 105 | "lint": { 106 | "builder": "@angular-devkit/build-angular:tslint", 107 | "options": { 108 | "tsConfig": [ 109 | "tsconfig.app.json", 110 | "tsconfig.spec.json", 111 | "e2e/tsconfig.json" 112 | ], 113 | "exclude": ["**/node_modules/**"] 114 | } 115 | }, 116 | "e2e": { 117 | "builder": "@angular-devkit/build-angular:protractor", 118 | "options": { 119 | "protractorConfig": "e2e/protractor.conf.js", 120 | "devServerTarget": "github-chat:serve" 121 | }, 122 | "configurations": { 123 | "production": { 124 | "devServerTarget": "github-chat:serve:production" 125 | } 126 | } 127 | } 128 | } 129 | } 130 | }, 131 | "defaultProject": "github-chat" 132 | } 133 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('github-chat app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser 19 | .manage() 20 | .logs() 21 | .get(logging.Type.BROWSER); 22 | expect(logs).not.toContain( 23 | jasmine.objectContaining({ 24 | level: logging.Level.SEVERE 25 | } as logging.Entry) 26 | ); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | basePath: "", 7 | frameworks: ["jasmine", "@angular-devkit/build-angular"], 8 | plugins: [ 9 | require("karma-jasmine"), 10 | require("karma-chrome-launcher"), 11 | require("karma-jasmine-html-reporter"), 12 | require("karma-coverage-istanbul-reporter"), 13 | require("@angular-devkit/build-angular/plugins/karma") 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require("path").join(__dirname, "./coverage/github-chat"), 20 | reports: ["html", "lcovonly", "text-summary"], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ["progress", "kjhtml"], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ["Chrome"], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "github-chat", 4 | "alias": "github-chat.now.sh", 5 | "routes": [ 6 | { 7 | "src": "/(assets/.+|.+\\.gif|.+\\.png|.+\\.jpg|.+\\.svg|.+\\.css|.+\\.js|.+\\.eot|.+\\.ttf|.+\\.woff|.+\\.woff2|.+\\.map)", 8 | "headers": { 9 | "cache-control": "max-age=31536000,immutable" 10 | }, 11 | "dest": "/$1" 12 | }, 13 | { 14 | "src": "/(.*).html", 15 | "headers": { 16 | "cache-control": "public,max-age=0,must-revalidate" 17 | }, 18 | "dest": "/$1.html" 19 | }, 20 | { 21 | "src": "/(.*).json", 22 | "headers": { 23 | "cache-control": "public,max-age=0,must-revalidate" 24 | }, 25 | "dest": "/$1.json" 26 | }, 27 | { 28 | "src": "/(.*)", 29 | "headers": { 30 | "cache-control": "public,max-age=0,must-revalidate" 31 | }, 32 | "dest": "/index.html" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-chat", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --port 4600", 7 | "build": "ng build --prod", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "deploy:now": "now ./dist -A ../now.json --token={{PASTE_YOUR_TOKEN}} && now --target production --token={{PASTE_YOUR_TOKEN}}", 12 | "deploy": "npm run lint && npm run build && npm run deploy:now" 13 | }, 14 | "author": { 15 | "name": "Juan Batty", 16 | "url": "https://juanbatty.com" 17 | }, 18 | "private": true, 19 | "dependencies": { 20 | "@angular/animations": "^10.1.6", 21 | "@angular/common": "^10.1.6", 22 | "@angular/compiler": "^10.1.6", 23 | "@angular/core": "^10.1.6", 24 | "@angular/fire": "^5.2.1", 25 | "@angular/forms": "^10.1.6", 26 | "@angular/platform-browser": "^10.1.6", 27 | "@angular/platform-browser-dynamic": "^10.1.6", 28 | "@angular/router": "^10.1.6", 29 | "@ctrl/ngx-emoji-mart": "0.14.0", 30 | "firebase": "6.4.2", 31 | "rxjs": "6.6.3", 32 | "tslib": "^2.0.0", 33 | "zone.js": "~0.10.2" 34 | }, 35 | "devDependencies": { 36 | "@angular-devkit/build-angular": "~0.1001.7", 37 | "@angular/cli": "~10.1.7", 38 | "@angular/compiler-cli": "^10.1.6", 39 | "@angular/language-service": "^10.1.6", 40 | "@types/node": "^12.11.1", 41 | "@types/jasmine": "~3.3.8", 42 | "@types/jasminewd2": "~2.0.3", 43 | "codelyzer": "^5.1.2", 44 | "jasmine-core": "~3.5.0", 45 | "jasmine-spec-reporter": "~5.0.0", 46 | "karma": "~5.0.0", 47 | "karma-chrome-launcher": "~3.1.0", 48 | "karma-coverage-istanbul-reporter": "~3.0.2", 49 | "karma-jasmine": "~4.0.0", 50 | "karma-jasmine-html-reporter": "^1.5.0", 51 | "protractor": "~7.0.0", 52 | "ts-node": "~7.0.0", 53 | "tslint": "~6.1.0", 54 | "typescript": "~4.0.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { AppComponent } from './app.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: 'chat', 8 | loadChildren: () => import('./chat/chat.module').then(m => m.ChatModule) 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forRoot(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class AppRoutingModule {} 17 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmbl1685/github-chat/0304d6b6ee1fe86d5769fd8a33bf179ac09ab4f7/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(waitForAsync(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [RouterTestingModule], 9 | declarations: [AppComponent] 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'github-chat'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('github-chat'); 23 | }); 24 | 25 | it('should render title', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('.content span').textContent).toContain( 30 | 'github-chat app is running!' 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | constructor() {} 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import { AngularFireModule } from '@angular/fire'; 7 | import { AngularFireAuthModule } from '@angular/fire/auth'; 8 | import { AngularFireDatabaseModule } from '@angular/fire/database'; 9 | import { AngularFireStorageModule } from '@angular/fire/storage'; 10 | import { CommonModule } from '@angular/common'; 11 | import { AuthService } from './core/services/auth.service'; 12 | import { AuthGuardService } from './core/services/auth-guard.service'; 13 | import { NavbarComponent } from './navbar/navbar.component'; 14 | import { environment } from '../environments/environment'; 15 | 16 | @NgModule({ 17 | declarations: [AppComponent, NavbarComponent], 18 | imports: [ 19 | CommonModule, 20 | BrowserModule, 21 | AppRoutingModule, 22 | AngularFireModule.initializeApp(environment.firebaseConfig), 23 | AngularFireAuthModule, 24 | AngularFireDatabaseModule, 25 | AngularFireStorageModule 26 | ], 27 | providers: [AuthService, AuthGuardService], 28 | bootstrap: [AppComponent, NavbarComponent] 29 | }) 30 | export class AppModule {} 31 | -------------------------------------------------------------------------------- /src/app/chat/chat-container/chat-container.component.html: -------------------------------------------------------------------------------- 1 |
2 |
8 |
9 | loading 15 |

{{ loadingMessage }}

16 |
17 | 18 |

19 | {{ messageIndicatorMessage }} 20 |

21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 | 35 | 36 | 41 | 42 | 46 | 47 | 53 | 54 | upload 62 | 63 | 72 | 73 | smile 81 |
82 | -------------------------------------------------------------------------------- /src/app/chat/chat-container/chat-container.component.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $white: #fff; 3 | 4 | .messages-container { 5 | border: 1px solid #d1d5da; 6 | background-color: $white; 7 | height: 80vh; 8 | vertical-align: middle; 9 | overflow: auto; 10 | border-bottom: none; 11 | border-radius: 3px; 12 | } 13 | 14 | .user { 15 | padding: 10px 0px 0px 10px; 16 | display: flex; 17 | align-items: center; 18 | cursor: pointer; 19 | } 20 | 21 | .user p { 22 | margin-left: 8px; 23 | } 24 | 25 | .upload { 26 | margin-left: 10px; 27 | padding-top: 10px; 28 | } 29 | 30 | .emoji { 31 | position: absolute; 32 | right: 0; 33 | margin-right: 25px; 34 | margin-top: 10px; 35 | } 36 | 37 | .emoji-mart { 38 | position: absolute; 39 | bottom: calc(50% - 40%); 40 | right: 60px; 41 | } 42 | 43 | .messages ::-webkit-scrollbar { 44 | width: 12px; 45 | background: $white; 46 | } 47 | 48 | .messages ::-webkit-scrollbar-thumb { 49 | background: #24292e; 50 | border-radius: 5px; 51 | border: 3px solid $white; 52 | } 53 | 54 | .chat-message { 55 | padding: 10px; 56 | width: 50%; 57 | text-align: left; 58 | } 59 | 60 | .send-message { 61 | border: 1px solid #d1d5da; 62 | background-color: $white; 63 | } 64 | 65 | .send-message img { 66 | cursor: pointer; 67 | } 68 | 69 | .message-time { 70 | color: #afafaf; 71 | font-size: 11.2px; 72 | } 73 | 74 | .new-img { 75 | max-width: 100%; 76 | max-height: 500px; 77 | } 78 | 79 | .input-message { 80 | width: 92%; 81 | outline: none; 82 | border: 1px solid transparent; 83 | padding: 10px; 84 | } 85 | 86 | .user-img { 87 | margin-top: -140px; 88 | margin-left: -20px; 89 | border: 1px solid #d1d5da; 90 | background-color: $white; 91 | } 92 | 93 | .time-end { 94 | text-align: end; 95 | } 96 | -------------------------------------------------------------------------------- /src/app/chat/chat-container/chat-container.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatContainerComponent } from './chat-container.component'; 4 | 5 | describe('ChatContainerComponent', () => { 6 | let component: ChatContainerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ChatContainerComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ChatContainerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/chat/chat-container/chat-container.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | ViewChild, 5 | ElementRef, 6 | OnDestroy 7 | } from '@angular/core'; 8 | import { FormControl, Validators } from '@angular/forms'; 9 | import { 10 | AngularFireUploadTask, 11 | AngularFireStorage 12 | } from '@angular/fire/storage'; 13 | import { Observable } from 'rxjs'; 14 | import { debounceTime } from 'rxjs/operators'; 15 | 16 | import { IUser } from 'src/app/core/models/user'; 17 | import { IMessage } from 'src/app/core/models/message'; 18 | import { ChatService } from 'src/app/core/services/chat.service'; 19 | 20 | @Component({ 21 | selector: 'app-chat-container', 22 | templateUrl: './chat-container.component.html', 23 | styleUrls: ['./chat-container.component.scss'] 24 | }) 25 | export class ChatContainerComponent implements OnInit, OnDestroy { 26 | loading: boolean; 27 | 28 | uploadFileStatus: boolean; 29 | showEmojiPicker: boolean; 30 | scrolltop: any; 31 | 32 | loadingMessage: string; 33 | messageIndicatorMessage: string; 34 | 35 | user: IUser; 36 | userTyping: IUser; 37 | 38 | messages: any[]; 39 | message: FormControl; 40 | 41 | task: AngularFireUploadTask; 42 | percentage: Observable; 43 | snapshot: Observable; 44 | downloadURL: Observable; 45 | isHovering: boolean; 46 | fileSizeLimit: number; 47 | uploadFileMessage: string; 48 | uploadFileExceededMessage: string; 49 | 50 | @ViewChild('chatContainer', { static: true }) 51 | chatContainer: ElementRef; 52 | 53 | @ViewChild('inputFile', { static: true }) 54 | inputFile: ElementRef; 55 | 56 | constructor( 57 | private chatService: ChatService, 58 | private storage: AngularFireStorage 59 | ) { 60 | this.loadingMessage = 'Loading messages...'; 61 | this.messageIndicatorMessage = 'No message currently. Be the first!!'; 62 | this.uploadFileMessage = 'Uploading file, please wait...'; 63 | this.uploadFileExceededMessage = 64 | 'You have exceeded the allowed limit (5.12MB)'; 65 | this.loading = false; 66 | this.messages = []; 67 | this.uploadFileStatus = false; 68 | this.showEmojiPicker = false; 69 | this.scrolltop = null; 70 | this.fileSizeLimit = 5367159; // 5.12MB 71 | this.message = new FormControl('', [Validators.required]); 72 | } 73 | 74 | ngOnInit() { 75 | this.checkUser(); 76 | this.getMessages(); 77 | this.isTypingObservable(); 78 | this.valuesChangesDebounceTime(); 79 | } 80 | 81 | ngOnDestroy() { 82 | this.isTypingChangeStatus(false); 83 | } 84 | 85 | valuesChangesDebounceTime() { 86 | this.message.valueChanges.pipe(debounceTime(200)).subscribe(res => { 87 | this.isTypingChangeStatus(false); 88 | }); 89 | } 90 | 91 | scrollHandler(event) { 92 | const scrollTop = event.target.scrollTop; 93 | if (scrollTop <= 5) { 94 | const lastKey = this.messages[0].key; 95 | this.getMessagesByLastKey(lastKey); 96 | } 97 | } 98 | 99 | toggleEmojiPicker() { 100 | this.showEmojiPicker = !this.showEmojiPicker; 101 | } 102 | 103 | addEmoji(event) { 104 | const text = `${this.message.value}${event.emoji.native}`; 105 | this.message.setValue(text); 106 | this.showEmojiPicker = false; 107 | } 108 | 109 | getMessagesByLastKey(lastKey: string) { 110 | this.loading = true; 111 | this.chatService 112 | .getMessagesByLastKey(this.messages.length, lastKey) 113 | .then(res => { 114 | const data = res.data; 115 | if (data.length !== 1) { 116 | this.messages.unshift(...data); 117 | this.scrolltop = 200; 118 | } 119 | this.loading = false; 120 | }) 121 | .catch(err => { 122 | this.loading = false; 123 | }); 124 | } 125 | 126 | messagesObservable() { 127 | this.chatService 128 | .messagesObservable() 129 | .snapshotChanges() 130 | .subscribe(res => this.pushChatMessages(res)); 131 | } 132 | 133 | pushChatMessages(messages: any[]) { 134 | const data: IMessage[] = messages.map(c => { 135 | const result = { 136 | key: c.payload.key, 137 | ...c.payload.val() 138 | }; 139 | return result; 140 | }); 141 | this.messages.push(...data); 142 | if (data.length > 0) { 143 | this.scrolltop = Math.pow( 144 | this.chatContainer.nativeElement.scrollHeight, 145 | 2 146 | ); 147 | } 148 | } 149 | 150 | getMessages(): Promise { 151 | return new Promise(async (resolve, reject) => { 152 | try { 153 | this.loading = true; 154 | const response = await this.chatService.getMessages(); 155 | if (response) { 156 | this.messages = response.data; 157 | this.loading = false; 158 | this.messagesObservable(); 159 | } 160 | resolve(response); 161 | } catch (err) { 162 | this.loading = false; 163 | reject(err); 164 | } 165 | }); 166 | } 167 | 168 | createMessage(event): void { 169 | const message = event.target.value; 170 | 171 | if (this.messageIsValid(message)) { 172 | const createdAt = new Date().toISOString(); 173 | 174 | const user = { 175 | id: this.user.id, 176 | name: this.usernameHandler(), 177 | avatar: this.user.avatar_url 178 | }; 179 | 180 | this.chatService.createMessage({ createdAt, message, user }); 181 | event.target.value = null; 182 | this.message.setValue(''); 183 | } 184 | } 185 | 186 | messageIsValid(message): boolean { 187 | return message.trim().length > 0; 188 | } 189 | 190 | checkUser(): void { 191 | const info = localStorage.getItem('userInfo'); 192 | this.user = info ? JSON.parse(info) : null; 193 | } 194 | 195 | clickInputFile(): void { 196 | this.inputFile.nativeElement.click(); 197 | } 198 | 199 | onFileChange(event: any): void { 200 | const files: FileList = event.target.files; 201 | const index = files.length; 202 | if (index > 0) { 203 | const fileSize = files.item(0).size; 204 | if (fileSize <= this.fileSizeLimit) { 205 | this.startUpload(files); 206 | return; 207 | } 208 | alert(this.uploadFileExceededMessage); 209 | } 210 | } 211 | 212 | startUpload(event: FileList): void { 213 | this.uploadFileStatus = true; 214 | 215 | const file = event.item(0); 216 | const firebaseFolder = 'chat-files'; 217 | const fileName = `${new Date().getTime()}_${file.name.toLocaleLowerCase()}`; 218 | const path = `${firebaseFolder}/${fileName}`; 219 | 220 | this.task = this.storage.upload(path, file); 221 | 222 | this.percentage = this.task.percentageChanges(); 223 | this.snapshot = this.task.snapshotChanges(); 224 | 225 | this.task.then(async res => { 226 | const url = await res.ref.getDownloadURL(); 227 | 228 | const createdAt = new Date().toISOString(); 229 | 230 | const user = { 231 | id: this.user.id, 232 | name: this.usernameHandler(), 233 | avatar: this.user.avatar_url 234 | }; 235 | 236 | const fileInfo = { 237 | type: file.type, 238 | fileName, 239 | url 240 | }; 241 | 242 | this.chatService.createMessage({ 243 | createdAt, 244 | message: '', 245 | user, 246 | file: fileInfo 247 | }); 248 | 249 | this.uploadFileStatus = false; 250 | }); 251 | } 252 | 253 | isTypingChangeStatus(status: boolean): void { 254 | const reference = this.user.id.toString(); 255 | this.chatService.isTyping(reference, status); 256 | } 257 | 258 | typing(event): void { 259 | const keys = 'cvxspwuaz'; 260 | const validation = event.key.trim().length === 1; 261 | this.typingCases(event, keys, validation); 262 | } 263 | 264 | typingCases(event: any, keys: string, validation: boolean): void { 265 | if (validation) { 266 | this.isTypingChangeStatus(true); 267 | } 268 | if (event.ctrlKey && keys.indexOf(event.key) !== -1) { 269 | this.isTypingChangeStatus(false); 270 | } 271 | } 272 | 273 | resetTyping(event: void): void { 274 | this.isTypingChangeStatus(false); 275 | } 276 | 277 | isTypingObservable(): void { 278 | this.chatService 279 | .isTypingObservable() 280 | .stateChanges(['child_changed']) 281 | .subscribe(res => { 282 | const user: IUser = res.payload.val(); 283 | this.userTyping = user.typing ? user : null; 284 | }); 285 | } 286 | 287 | usernameHandler(): string { 288 | return this.user.name ? this.user.name : this.user.login; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/app/chat/chat-container/chat-message/chat-message.component.html: -------------------------------------------------------------------------------- 1 |
2 |
6 |
13 | avatar 20 | 21 | {{ message.user.name }} 22 | 23 | 24 | 25 |

26 | 27 |
28 | 41 | 42 | img 49 | 59 | 📦 {{ message.file?.fileName }} 62 |
63 | 64 |
65 | {{ 66 | message.createdAt | date: "medium" 67 | }} 68 |
69 |
70 |
71 |
72 | -------------------------------------------------------------------------------- /src/app/chat/chat-container/chat-message/chat-message.component.scss: -------------------------------------------------------------------------------- 1 | .messages-container { 2 | border: 1px solid #d1d5da; 3 | background-color: #fff; 4 | height: 80vh; 5 | vertical-align: middle; 6 | overflow: auto; 7 | border-bottom: none; 8 | border-radius: 3px; 9 | } 10 | 11 | .user { 12 | padding: 10px 0px 0px 10px; 13 | display: flex; 14 | align-items: center; 15 | cursor: pointer; 16 | } 17 | 18 | .user p { 19 | margin-left: 8px; 20 | } 21 | 22 | .upload { 23 | margin-left: 10px; 24 | padding-top: 10px; 25 | } 26 | 27 | .emoji { 28 | position: absolute; 29 | right: 0; 30 | margin-right: 25px; 31 | margin-top: 10px; 32 | } 33 | 34 | .emoji-mart { 35 | position: absolute; 36 | bottom: calc(50% - 330px); 37 | right: 60px; 38 | } 39 | 40 | .messages ::-webkit-scrollbar { 41 | width: 12px; 42 | background: #fff; 43 | } 44 | 45 | .messages ::-webkit-scrollbar-thumb { 46 | background: #24292e; 47 | border-radius: 5px; 48 | border: 3px solid #fff; 49 | } 50 | 51 | .chat-message { 52 | padding: 10px; 53 | width: 50%; 54 | text-align: left; 55 | } 56 | 57 | .grayContainer { 58 | background-color: #20e43a48; 59 | border: 1px solid #d1d5da; 60 | } 61 | 62 | .otherContainer { 63 | background-color: #f6f8fa; 64 | border: 1px solid #d1d5da; 65 | } 66 | 67 | .send-message { 68 | border: 1px solid #d1d5da; 69 | background-color: #fff; 70 | } 71 | 72 | .send-message img { 73 | cursor: pointer; 74 | } 75 | 76 | .is-typing { 77 | position: absolute; 78 | margin-top: -20px; 79 | font-size: 10.5px; 80 | background: #fff; 81 | margin-left: 12px; 82 | border: 1px solid #d1d5da; 83 | padding-left: 50px; 84 | padding-right: 50px; 85 | } 86 | 87 | .message-time { 88 | color: #afafaf; 89 | font-size: 11.2px; 90 | } 91 | 92 | .new-img { 93 | max-width: 100%; 94 | max-height: 500px; 95 | } 96 | 97 | .input-message { 98 | width: 92%; 99 | outline: none; 100 | border: 1px solid transparent; 101 | padding: 10px; 102 | } 103 | 104 | .user-img { 105 | margin-top: -140px; 106 | margin-left: -20px; 107 | border: 1px solid #d1d5da; 108 | background-color: white; 109 | } 110 | 111 | .time-end { 112 | text-align: end; 113 | } 114 | 115 | .spinner { 116 | margin: 100px auto 0; 117 | width: 8px; 118 | text-align: center; 119 | } 120 | 121 | .spinner > div { 122 | width: 6px; 123 | height: 6px; 124 | background-color: #333; 125 | 126 | border-radius: 100%; 127 | display: inline-block; 128 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 129 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 130 | } 131 | 132 | .spinner .bounce1 { 133 | -webkit-animation-delay: -0.32s; 134 | animation-delay: -0.32s; 135 | } 136 | 137 | .spinner .bounce2 { 138 | -webkit-animation-delay: -0.16s; 139 | animation-delay: -0.16s; 140 | } 141 | 142 | @-webkit-keyframes sk-bouncedelay { 143 | 0%, 144 | 80%, 145 | 100% { 146 | -webkit-transform: scale(0); 147 | } 148 | 40% { 149 | -webkit-transform: scale(1); 150 | } 151 | } 152 | 153 | @keyframes sk-bouncedelay { 154 | 0%, 155 | 80%, 156 | 100% { 157 | -webkit-transform: scale(0); 158 | transform: scale(0); 159 | } 160 | 40% { 161 | -webkit-transform: scale(1); 162 | transform: scale(1); 163 | } 164 | } 165 | 166 | @media only screen and (max-width: 767px) { 167 | .align-messages { 168 | margin: 0 auto; 169 | width: 100%; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/app/chat/chat-container/chat-message/chat-message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatMessageComponent } from './chat-message.component'; 4 | 5 | describe('ChatMessageComponent', () => { 6 | let component: ChatMessageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ChatMessageComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatMessageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/chat-container/chat-message/chat-message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { IMessage } from 'src/app/core/models/message'; 3 | import { IUser } from 'src/app/core/models/user'; 4 | import { EventService } from 'src/app/core/services/event.service'; 5 | 6 | @Component({ 7 | selector: 'app-chat-message', 8 | templateUrl: './chat-message.component.html', 9 | styleUrls: ['./chat-message.component.scss'] 10 | }) 11 | export class ChatMessageComponent implements OnInit { 12 | user: IUser; 13 | @Input() messages: Array; 14 | 15 | constructor(private eventService: EventService) {} 16 | 17 | ngOnInit() { 18 | this.checkUser(); 19 | } 20 | 21 | checkUser() { 22 | const info = localStorage.getItem('userInfo'); 23 | this.user = info ? JSON.parse(info) : null; 24 | } 25 | 26 | openImg(url: string) { 27 | this.eventService.emit('OPEN_IMAGE_MODAL', { imgUrl: url }); 28 | } 29 | 30 | formatText(text) { 31 | const urlRegex = /(https?:\/\/[^\s]+)/g; 32 | return text.replace(urlRegex, url => { 33 | return `${url}`; 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/chat/chat-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ChatComponent } from './chat.component'; 4 | import { AuthGuardService } from '../core/services/auth-guard.service'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: ChatComponent, 10 | canActivate: [AuthGuardService] 11 | }, 12 | ]; 13 | 14 | @NgModule({ 15 | imports: [RouterModule.forChild(routes)], 16 | exports: [RouterModule] 17 | }) 18 | export class ChatRoutingModule {} 19 | -------------------------------------------------------------------------------- /src/app/chat/chat-users/chat-users.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 | loading 11 |

{{ loadingMessage }}

12 |
13 | 14 | avatar 21 | 25 |

{{ user.name ? user.name : user.login }}


27 |
28 |
29 | -------------------------------------------------------------------------------- /src/app/chat/chat-users/chat-users.component.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $user-connected: #1ed760; 3 | $user-disconnected: #f44336; 4 | 5 | .users-container { 6 | border: 1px solid #d1d5da; 7 | background-color: #fff; 8 | height: 85.5vh; 9 | overflow: auto; 10 | border-radius: 3px; 11 | } 12 | 13 | .users ::-webkit-scrollbar { 14 | width: 12px; 15 | background: #fff; 16 | } 17 | 18 | .users ::-webkit-scrollbar-thumb { 19 | background: #24292e; 20 | border-radius: 5px; 21 | border: 3px solid #fff; 22 | } 23 | 24 | .user { 25 | padding: 10px 0px 0px 10px; 26 | display: flex; 27 | align-items: center; 28 | cursor: pointer; 29 | } 30 | 31 | .user p { 32 | margin-left: 8px; 33 | } 34 | 35 | .status { 36 | height: 10px; 37 | width: 10px; 38 | border-radius: 10px; 39 | margin-left: -10px; 40 | margin-top: 30px; 41 | } 42 | 43 | .connected { 44 | background-color: $user-connected; 45 | border: 2px solid #fff; 46 | height: 12px; 47 | width: 12px; 48 | } 49 | 50 | .disconnected { 51 | background-color: $user-disconnected; 52 | border: 2px solid #fff; 53 | height: 12px; 54 | width: 12px; 55 | } 56 | -------------------------------------------------------------------------------- /src/app/chat/chat-users/chat-users.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatUsersComponent } from './chat-users.component'; 4 | 5 | describe('ChatUsersComponent', () => { 6 | let component: ChatUsersComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ChatUsersComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatUsersComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/chat-users/chat-users.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { IUser } from 'src/app/core/models/user'; 4 | import { UserService } from 'src/app/core/services/user.service'; 5 | 6 | @Component({ 7 | selector: 'app-chat-users', 8 | templateUrl: './chat-users.component.html', 9 | styleUrls: ['./chat-users.component.scss'] 10 | }) 11 | export class ChatUsersComponent implements OnInit { 12 | loading: boolean; 13 | users: IUser[]; 14 | loadingMessage: string; 15 | title: string; 16 | 17 | constructor(private userService: UserService, private router: Router) { 18 | this.loading = false; 19 | this.users = []; 20 | this.loadingMessage = 'Loading users...'; 21 | this.title = 'Users'; 22 | } 23 | 24 | ngOnInit() { 25 | this.getUsers(); 26 | this.userConnectedObservable(); 27 | } 28 | 29 | gitHubProfile(user: IUser) { 30 | window.open(user.html_url, '_blank'); 31 | } 32 | 33 | getUsers() { 34 | this.loading = true; 35 | return new Promise(async (resolve, reject) => { 36 | try { 37 | const response = await this.userService.getUsers(); 38 | this.users = response; 39 | this.loading = false; 40 | resolve(this.users); 41 | } catch (err) { 42 | this.loading = false; 43 | reject(err); 44 | } 45 | }); 46 | } 47 | 48 | userConnectedObservable() { 49 | this.userService 50 | .userConnectedObservable() 51 | .stateChanges(['child_changed']) 52 | .subscribe(res => { 53 | const user: IUser = res.payload.val(); 54 | 55 | this.users.forEach(data => { 56 | if (data.id === user.id) { 57 | data.status = user.status; 58 | } 59 | }); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/chat/chat.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/chat/chat.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmbl1685/github-chat/0304d6b6ee1fe86d5769fd8a33bf179ac09ab4f7/src/app/chat/chat.component.scss -------------------------------------------------------------------------------- /src/app/chat/chat.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatComponent } from './chat.component'; 4 | 5 | describe('ChatComponent', () => { 6 | let component: ChatComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ChatComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/chat.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, HostListener } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-chat', 5 | templateUrl: './chat.component.html', 6 | styleUrls: ['./chat.component.scss'] 7 | }) 8 | export class ChatComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ChatUsersComponent } from './chat-users/chat-users.component'; 4 | import { ChatContainerComponent } from './chat-container/chat-container.component'; 5 | import { ChatRoutingModule } from './chat-routing.module'; 6 | import { ChatComponent } from './chat.component'; 7 | import { SharedModule } from '../shared/shared.module'; 8 | import { UserService } from '../core/services/user.service'; 9 | import { ChatService } from '../core/services/chat.service'; 10 | import { ChatMessageComponent } from './chat-container/chat-message/chat-message.component'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | ChatComponent, 15 | ChatUsersComponent, 16 | ChatContainerComponent, 17 | ChatMessageComponent 18 | ], 19 | imports: [SharedModule, CommonModule, ChatRoutingModule], 20 | providers: [UserService, ChatService] 21 | }) 22 | export class ChatModule {} 23 | -------------------------------------------------------------------------------- /src/app/core/models/message.ts: -------------------------------------------------------------------------------- 1 | export interface IMessage { 2 | message: string; 3 | file?: IFileMessage; 4 | createdAt: string; 5 | user: IUserMessage; 6 | } 7 | 8 | interface IUserMessage { 9 | id: number; 10 | name: string; 11 | avatar: string; 12 | } 13 | 14 | interface IFileMessage { 15 | type: string; 16 | fileName: string; 17 | url: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/core/models/user.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | avatar_url: string; 3 | bio: string; 4 | blog: string; 5 | company: string; 6 | created_at: string; 7 | email: string; 8 | events_url: string; 9 | followers: number; 10 | followers_url: string; 11 | following: number; 12 | following_url: string; 13 | gists_url: string; 14 | gravatar_id: string; 15 | hireable: null; 16 | html_url: string; 17 | id: number; 18 | location: string; 19 | login: string; 20 | name: string; 21 | node_id: string; 22 | organizations_url: string; 23 | public_gists: number; 24 | public_repos: number; 25 | received_events_url: string; 26 | repos_url: string; 27 | site_admin: boolean; 28 | starred_url: string; 29 | subscriptions_url: string; 30 | type: string; 31 | updated_at: string; 32 | url: string; 33 | status: boolean; 34 | typing?: boolean; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/core/services/auth-guard.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthGuardService } from './auth-guard.service'; 4 | 5 | describe('AuthGuardService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AuthGuardService = TestBed.inject(AuthGuardService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/core/services/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AuthService } from '../services/auth.service'; 3 | import { AngularFireAuth } from '@angular/fire/auth'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class AuthGuardService { 9 | constructor(public afAuth: AngularFireAuth, public authSevice: AuthService) { } 10 | 11 | canActivate() { 12 | return new Promise((resolve, reject) => { 13 | this.afAuth.auth.onAuthStateChanged(async (user) => { 14 | if (user !== null) { 15 | const token = await user.getIdToken(true); 16 | const validate = token.length > 0 ? true : false; 17 | resolve(validate); 18 | } else { 19 | reject(false); 20 | } 21 | }); 22 | }); 23 | } 24 | 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/app/core/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AuthService = TestBed.inject(AuthService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/core/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AngularFireAuth } from '@angular/fire/auth'; 3 | 4 | import { auth as firebaseAuth } from 'firebase/app'; 5 | 6 | @Injectable() 7 | export class AuthService { 8 | constructor(private afAuth: AngularFireAuth) {} 9 | 10 | githubLogin() { 11 | return new Promise(async (resolve, reject) => { 12 | try { 13 | const provider = new firebaseAuth.GithubAuthProvider(); 14 | const response = await this.afAuth.auth.signInWithPopup(provider); 15 | resolve(response); 16 | } catch (err) { 17 | reject(err); 18 | } 19 | }); 20 | } 21 | 22 | facebookLogin() { 23 | return new Promise(async (resolve, reject) => { 24 | try { 25 | const provider = new firebaseAuth.FacebookAuthProvider(); 26 | const response = await this.afAuth.auth.signInWithPopup(provider); 27 | resolve(response); 28 | } catch (err) { 29 | reject(err); 30 | } 31 | }); 32 | } 33 | 34 | twitterLogin() { 35 | return new Promise(async (resolve, reject) => { 36 | try { 37 | const provider = new firebaseAuth.TwitterAuthProvider(); 38 | const response = await this.afAuth.auth.signInWithPopup(provider); 39 | resolve(response); 40 | } catch (err) { 41 | reject(err); 42 | } 43 | }); 44 | } 45 | 46 | googleLogin() { 47 | return new Promise(async (resolve, reject) => { 48 | try { 49 | const provider = new firebaseAuth.GoogleAuthProvider(); 50 | const response = await this.afAuth.auth.signInWithPopup(provider); 51 | resolve(response); 52 | } catch (err) { 53 | reject(err); 54 | } 55 | }); 56 | } 57 | 58 | logout() { 59 | return new Promise((resolve, reject) => { 60 | if (firebaseAuth().currentUser) { 61 | this.afAuth.auth.signOut(); 62 | resolve(true); 63 | } else { 64 | reject(false); 65 | } 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/core/services/chat.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatService } from './chat.service'; 4 | 5 | describe('ChatService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ChatService = TestBed.inject(ChatService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/core/services/chat.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AngularFireDatabase, AngularFireList } from '@angular/fire/database'; 3 | import { IMessage } from '../models/message'; 4 | import { environment } from '../../../environments/environment'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class ChatService { 10 | chatMessagesCollection = environment.collectionsName.chatMessagesCollection; 11 | chatMembersCollection = environment.collectionsName.chatMemberCollection; 12 | 13 | constructor(private db: AngularFireDatabase) {} 14 | 15 | createMessage(message: IMessage): void { 16 | const id = this.db.createPushId(); 17 | this.db.database.ref(`${this.chatMessagesCollection}/${id}`).set(message); 18 | } 19 | 20 | getMessagesByLastKey(numberItems: number, lastKey: string): Promise { 21 | return new Promise(async (resolve, reject) => { 22 | try { 23 | const data = await this.db.database 24 | .ref(this.chatMessagesCollection) 25 | .orderByKey() 26 | .endAt(lastKey) 27 | .limitToLast(numberItems + 1) 28 | .once('value'); 29 | 30 | const response: any[] = data.val(); 31 | const keysList = Array.from(Object.keys(response)); 32 | const lastKeyReturned = keysList[keysList.length - 1]; 33 | 34 | const messages = Object.values(response).map((obj, i) => { 35 | return { 36 | key: Object.keys(response)[i], 37 | ...obj 38 | }; 39 | }); 40 | 41 | messages.pop(); 42 | 43 | resolve({ data: messages, lastKey: lastKeyReturned }); 44 | } catch (err) { 45 | reject(err); 46 | } 47 | }); 48 | } 49 | 50 | getMessages(): Promise { 51 | return new Promise(async (resolve, reject) => { 52 | try { 53 | const data = await this.db.database 54 | .ref(this.chatMessagesCollection) 55 | .limitToLast(10) 56 | .once('value'); 57 | 58 | const response: any[] = data.val(); 59 | let lastKeyReturned = null; 60 | let messages = []; 61 | 62 | if (response !== null) { 63 | const keysList = Array.from(Object.keys(response)); 64 | lastKeyReturned = keysList[keysList.length - 1]; 65 | 66 | messages = Object.values(response).map((obj, i) => { 67 | return { 68 | key: Object.keys(response)[i], 69 | ...obj 70 | }; 71 | }); 72 | 73 | messages.pop(); 74 | } 75 | 76 | resolve({ data: messages, lastKey: lastKeyReturned }); 77 | } catch (err) { 78 | reject(err); 79 | } 80 | }); 81 | } 82 | 83 | isTyping(reference: string, status: boolean): void { 84 | this.db 85 | .list(`${this.chatMembersCollection}/${reference}`) 86 | .set('typing', status); 87 | } 88 | 89 | isTypingObservable(): AngularFireList { 90 | return this.db.list(`${this.chatMembersCollection}`); 91 | } 92 | 93 | messagesObservable(): AngularFireList { 94 | return this.db.list(this.chatMessagesCollection, res => res.limitToLast(1)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/app/core/services/event.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { EventService } from './event.service'; 4 | 5 | describe('EventService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: EventService = TestBed.inject(EventService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/core/services/event.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class EventService { 8 | private dataSource; 9 | private data; 10 | 11 | constructor() { 12 | this.initialize(); 13 | } 14 | 15 | initialize() { 16 | this.dataSource = new BehaviorSubject({}); 17 | this.data = this.dataSource.asObservable(); 18 | } 19 | 20 | emit(type: EventType, data: any) { 21 | this.dataSource.next({ type, data }); 22 | } 23 | 24 | listen() { 25 | return this.data; 26 | } 27 | 28 | finish() { 29 | this.dataSource.next({}); 30 | } 31 | } 32 | 33 | export type EventType = 'OPEN_IMAGE_MODAL'; 34 | -------------------------------------------------------------------------------- /src/app/core/services/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserService } from './user.service'; 4 | 5 | describe('UserService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: UserService = TestBed.inject(UserService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/core/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { IUser } from '../models/user'; 3 | import { AngularFireDatabase, AngularFireList } from '@angular/fire/database'; 4 | import { environment } from '../../../environments/environment'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class UserService { 10 | chatMemberCollection = environment.collectionsName.chatMemberCollection; 11 | 12 | constructor(private db: AngularFireDatabase) {} 13 | 14 | createUser(user: IUser): void { 15 | const reference = user.id.toString(); 16 | this.db.list(`${this.chatMemberCollection}`).set(reference, user); 17 | } 18 | 19 | userConnectedStatus(user: IUser): void { 20 | const reference = user.id.toString(); 21 | this.db.list(`${this.chatMemberCollection}`).set(reference, user); 22 | } 23 | 24 | updateUserConnectedStatus(reference: string, status: boolean): void { 25 | this.db 26 | .list(`${this.chatMemberCollection}/${reference}`) 27 | .set('status', status); 28 | } 29 | 30 | getUsers(): Promise { 31 | return new Promise(async (resolve, reject) => { 32 | try { 33 | const data = await this.db.database 34 | .ref(`${this.chatMemberCollection}`) 35 | .once('value'); 36 | const users: any = Object.values(data.val()).map(user => user); 37 | resolve(users); 38 | } catch (err) { 39 | reject(err); 40 | } 41 | }); 42 | } 43 | 44 | userConnectedObservable(): AngularFireList { 45 | return this.db.list(`${this.chatMemberCollection}`); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.html: -------------------------------------------------------------------------------- 1 | 25 |

26 | 27 |
28 |
29 |
30 |

Chat for GitHub Developers?

31 | 32 |
33 |
34 | img 35 |

{{ perk.title }}

36 |

37 | {{ perk.description }} 38 |

39 |
40 |
41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.scss: -------------------------------------------------------------------------------- 1 | $nav-height: 60px; 2 | 3 | .github-btn { 4 | background-color: #444444; 5 | color: #fff; 6 | height: 40px; 7 | width: 150px; 8 | border: 1px solid transparent; 9 | cursor: pointer; 10 | } 11 | 12 | .toolbar { 13 | height: $nav-height; 14 | margin: -8px; 15 | display: flex; 16 | align-items: center; 17 | background-color: #24292e; 18 | color: white; 19 | } 20 | 21 | .toolbar img { 22 | margin: 0 16px; 23 | } 24 | 25 | .toolbar #twitter-logo { 26 | height: 40px; 27 | margin: 0 16px; 28 | } 29 | 30 | .toolbar #twitter-logo:hover { 31 | opacity: 0.8; 32 | } 33 | 34 | .nav-fixed { 35 | position: fixed; 36 | top: 0; 37 | right: 0; 38 | left: 0; 39 | z-index: 1; 40 | } 41 | 42 | @media only screen and (max-width: 570px) { 43 | .title { 44 | display: none; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NavbarComponent } from './navbar.component'; 4 | 5 | describe('NavbarComponent', () => { 6 | let component: NavbarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [NavbarComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NavbarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostListener, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { IUser } from 'src/app/core/models/user'; 4 | import { UserService } from 'src/app/core/services/user.service'; 5 | import { AuthService } from 'src/app/core/services/auth.service'; 6 | 7 | @Component({ 8 | selector: 'app-navbar', 9 | templateUrl: './navbar.component.html', 10 | styleUrls: ['./navbar.component.scss'] 11 | }) 12 | export class NavbarComponent implements OnInit { 13 | user: IUser; 14 | status: boolean; 15 | title: string; 16 | buttonTitle: string; 17 | perks: any[]; 18 | 19 | constructor( 20 | private userService: UserService, 21 | private authService: AuthService, 22 | private router: Router 23 | ) { 24 | this.status = false; 25 | this.title = 'Chat for GitHub Developers'; 26 | this.buttonTitle = 'Sign In With GitHub'; 27 | this.checkUser(); 28 | this.fillPerks(); 29 | } 30 | 31 | ngOnInit(): void { 32 | this.updateUserConnectedStatus(true); 33 | } 34 | 35 | @HostListener('window:beforeunload', ['$event']) 36 | changeUserStatus($event): void { 37 | this.updateUserConnectedStatus(false); 38 | } 39 | 40 | updateUserConnectedStatus(status: boolean): void { 41 | if (this.user) { 42 | this.userService.updateUserConnectedStatus( 43 | this.user.id.toString(), 44 | status 45 | ); 46 | } 47 | } 48 | 49 | githubLogin() { 50 | return new Promise(async (resolve, reject) => { 51 | try { 52 | const response = await this.authService.githubLogin(); 53 | this.user = response.additionalUserInfo.profile; 54 | this.status = true; 55 | this.user.status = this.status; 56 | this.user.typing = false; 57 | localStorage.setItem('userInfo', JSON.stringify(this.user)); 58 | this.router.navigateByUrl('/chat'); 59 | this.userService.createUser(this.user); 60 | resolve(this.user); 61 | } catch (err) { 62 | reject(err); 63 | } 64 | }); 65 | } 66 | 67 | checkUser() { 68 | const info = localStorage.getItem('userInfo'); 69 | if (info) { 70 | this.user = JSON.parse(info); 71 | this.status = true; 72 | this.user.status = this.status; 73 | this.user.typing = false; 74 | this.router.navigateByUrl('/chat'); 75 | } 76 | } 77 | 78 | logout() { 79 | return new Promise(async (resolve, reject) => { 80 | try { 81 | const response = await this.authService.logout(); 82 | if (response) { 83 | this.status = false; 84 | this.updateUserConnectedStatus(false); 85 | localStorage.removeItem('userInfo'); 86 | this.router.navigateByUrl('/'); 87 | } 88 | resolve(response); 89 | } catch (err) { 90 | reject(err); 91 | } 92 | }); 93 | } 94 | 95 | fillPerks() { 96 | this.perks = [ 97 | { 98 | img: '../assets/img/shared.svg', 99 | title: 'Shared Files', 100 | description: 101 | ' Send all kinds of files in order to share knowledge, pdf, images,videos, tutorial, etc.' 102 | }, 103 | { 104 | img: '../assets/img/doubt.svg', 105 | title: 'Solve doubts in real time', 106 | description: 107 | 'The chat allows you to share new repositories, and answer questions and comment on code from other developers.' 108 | }, 109 | { 110 | img: '../assets/img/world.svg', 111 | title: 'Contact different developers', 112 | description: 'Meet and contact different developers around the world.' 113 | } 114 | ]; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/app/shared/loading/loading.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 |
7 |
8 | 9 | {{ boldMessage }}{{ message }} 11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/shared/loading/loading.component.scss: -------------------------------------------------------------------------------- 1 | .is-typing { 2 | position: absolute; 3 | margin-top: -20px; 4 | font-size: 10.5px; 5 | background: #fff; 6 | margin-left: 12px; 7 | border: 1px solid #d1d5da; 8 | padding-left: 50px; 9 | padding-right: 50px; 10 | } 11 | 12 | .spinner { 13 | margin: 100px auto 0; 14 | width: 8px; 15 | text-align: center; 16 | } 17 | 18 | .spinner > div { 19 | width: 6px; 20 | height: 6px; 21 | background-color: #333; 22 | 23 | border-radius: 100%; 24 | display: inline-block; 25 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 26 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 27 | } 28 | 29 | .spinner .bounce1 { 30 | -webkit-animation-delay: -0.32s; 31 | animation-delay: -0.32s; 32 | } 33 | 34 | .spinner .bounce2 { 35 | -webkit-animation-delay: -0.16s; 36 | animation-delay: -0.16s; 37 | } 38 | 39 | @-webkit-keyframes sk-bouncedelay { 40 | 0%, 41 | 80%, 42 | 100% { 43 | -webkit-transform: scale(0); 44 | } 45 | 40% { 46 | -webkit-transform: scale(1); 47 | } 48 | } 49 | 50 | @keyframes sk-bouncedelay { 51 | 0%, 52 | 80%, 53 | 100% { 54 | -webkit-transform: scale(0); 55 | transform: scale(0); 56 | } 57 | 40% { 58 | -webkit-transform: scale(1); 59 | transform: scale(1); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/shared/loading/loading.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoadingComponent } from './loading.component'; 4 | 5 | describe('LoadingComponent', () => { 6 | let component: LoadingComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [LoadingComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoadingComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/loading/loading.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-loading', 5 | templateUrl: './loading.component.html', 6 | styleUrls: ['./loading.component.scss'] 7 | }) 8 | export class LoadingComponent implements OnInit { 9 | @Input() show: boolean; 10 | @Input() message: string; 11 | @Input() boldMessage?: string; 12 | 13 | constructor() {} 14 | 15 | ngOnInit() {} 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/modal/modal.component.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/app/shared/modal/modal.component.scss: -------------------------------------------------------------------------------- 1 | .img-responsive-modal { 2 | margin: auto; 3 | max-width: 80%; 4 | max-height: 80%; 5 | } 6 | 7 | .modal { 8 | position: fixed; 9 | width: 100vw; 10 | height: 100vh; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | background-color: rgba(0, 0, 0, 0.829); 15 | top: 0; 16 | left: 0; 17 | right: 0; 18 | z-index: 99; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/modal/modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ModalComponent } from './modal.component'; 4 | 5 | describe('ModalComponent', () => { 6 | let component: ModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ModalComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/modal/modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, HostListener } from '@angular/core'; 2 | import { EventService } from 'src/app/core/services/event.service'; 3 | 4 | @Component({ 5 | selector: 'app-modal', 6 | templateUrl: './modal.component.html', 7 | styleUrls: ['./modal.component.scss'] 8 | }) 9 | export class ModalComponent implements OnInit { 10 | imgUrl: string; 11 | enable: boolean; 12 | 13 | constructor(private eventService: EventService) { 14 | this.enable = false; 15 | } 16 | 17 | @HostListener('window:keyup', ['$event']) 18 | keyEvent(event: KeyboardEvent) { 19 | if (event.key === 'Escape' && this.enable) { 20 | this.hideModal(); 21 | } 22 | } 23 | 24 | ngOnInit() { 25 | this.eventService.listen().subscribe(res => { 26 | switch (res.type) { 27 | case 'OPEN_IMAGE_MODAL': 28 | this.imgUrl = res.data.imgUrl; 29 | this.enable = true; 30 | break; 31 | } 32 | }); 33 | } 34 | 35 | hideModal() { 36 | this.enable = false; 37 | this.eventService.finish(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | /* Modules */ 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | import { RouterModule } from '@angular/router'; 5 | import { HttpClientModule } from '@angular/common/http'; 6 | import { PickerModule } from '@ctrl/ngx-emoji-mart'; 7 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 8 | import { LoadingComponent } from './loading/loading.component'; 9 | import { ModalComponent } from './modal/modal.component'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | FormsModule, 14 | ReactiveFormsModule, 15 | CommonModule, 16 | HttpClientModule, 17 | RouterModule, 18 | PickerModule 19 | ], 20 | declarations: [LoadingComponent, ModalComponent], 21 | entryComponents: [], 22 | exports: [ 23 | LoadingComponent, 24 | ModalComponent, 25 | FormsModule, 26 | ReactiveFormsModule, 27 | CommonModule, 28 | HttpClientModule, 29 | RouterModule, 30 | PickerModule 31 | ] 32 | }) 33 | export class SharedModule {} 34 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmbl1685/github-chat/0304d6b6ee1fe86d5769fd8a33bf179ac09ab4f7/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/css/grid.min.css: -------------------------------------------------------------------------------- 1 | */html{box-sizing:border-box;-ms-overflow-style:scrollbar}*,::after,::before{box-sizing:inherit}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}} -------------------------------------------------------------------------------- /src/assets/img/doubt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 17 | 21 | 24 | 25 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/assets/img/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/img/octocat-spinner-128.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmbl1685/github-chat/0304d6b6ee1fe86d5769fd8a33bf179ac09ab4f7/src/assets/img/octocat-spinner-128.gif -------------------------------------------------------------------------------- /src/assets/img/shared.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 12 | 18 | 21 | 22 | 24 | 26 | 27 | 29 | 31 | 32 | 34 | 36 | 38 | 40 | 41 | 43 | 45 | 48 | 50 | 52 | 55 | 57 | 59 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/assets/img/smile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/assets/img/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/assets/img/world.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "gcm_sender_id": "103953800507" 3 | } 4 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | const firebaseConfig = { 2 | apiKey: '{{PASTE_YOUR_API_KEY}}', 3 | authDomain: '{{PASTE_YOUR_AUTH_DOMAIN}}', 4 | databaseURL: '{{PASTE_YOUR_DATABASE_URL}}', 5 | projectId: '{{PASTE_YOUR_PROJECT_ID}}', 6 | storageBucket: '{{PASTE_YOUR_STORAGE_BUCKET}}', 7 | messagingSenderId: '{{PASTE_YOUR_MESSAGING_SENDER_ID}}', 8 | appId: '{{PASTE_YOUR_APP_ID}}' 9 | }; 10 | 11 | const collectionsName = { 12 | chatMemberCollection: 'chat-members', 13 | chatMessagesCollection: 'chat-messages' 14 | }; 15 | 16 | const production = true; 17 | 18 | export const environment = { 19 | production, 20 | firebaseConfig, 21 | collectionsName 22 | }; 23 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | const firebaseConfig = { 2 | apiKey: '{{PASTE_YOUR_API_KEY}}', 3 | authDomain: '{{PASTE_YOUR_AUTH_DOMAIN}}', 4 | databaseURL: '{{PASTE_YOUR_DATABASE_URL}}', 5 | projectId: '{{PASTE_YOUR_PROJECT_ID}}', 6 | storageBucket: '{{PASTE_YOUR_STORAGE_BUCKET}}', 7 | messagingSenderId: '{{PASTE_YOUR_MESSAGING_SENDER_ID}}', 8 | appId: '{{PASTE_YOUR_APP_ID}}' 9 | }; 10 | 11 | const collectionsName = { 12 | chatMemberCollection: 'chat-members-dev', 13 | chatMessagesCollection: 'chat-messages-dev' 14 | }; 15 | 16 | const production = false; 17 | 18 | export const environment = { 19 | production, 20 | firebaseConfig, 21 | collectionsName 22 | }; 23 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmbl1685/github-chat/0304d6b6ee1fe86d5769fd8a33bf179ac09ab4f7/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | Chat for Developers on Github
-------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /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.ts'; 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 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, 3 | Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 4 | } 5 | 6 | .hidden { 7 | display: none; 8 | } 9 | 10 | body { 11 | font-size: 14px; 12 | color: #333; 13 | box-sizing: border-box; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | background-color: #f6f8fa; 17 | } 18 | 19 | .cursor-pointer { 20 | cursor: pointer; 21 | } 22 | 23 | h1, 24 | h2, 25 | h3, 26 | h4, 27 | h5, 28 | h6 { 29 | margin: 8px 0; 30 | } 31 | 32 | p { 33 | margin: 0; 34 | } 35 | 36 | a { 37 | color: #4a5699 !important; 38 | 39 | &:hover { 40 | border-bottom: 1px solid #4a5699; 41 | } 42 | } 43 | 44 | .spacer { 45 | flex: 1; 46 | } 47 | 48 | .content { 49 | display: flex; 50 | margin: 32px auto; 51 | padding: 0 16px; 52 | max-width: 960px; 53 | flex-direction: column; 54 | align-items: center; 55 | } 56 | 57 | .text-center { 58 | text-align: center; 59 | } 60 | 61 | svg.material-icons { 62 | height: 24px; 63 | width: auto; 64 | } 65 | 66 | svg.material-icons:not(:last-child) { 67 | margin-right: 8px; 68 | } 69 | 70 | .card svg.material-icons path { 71 | fill: #888; 72 | } 73 | 74 | .card-container { 75 | display: flex; 76 | flex-wrap: wrap; 77 | justify-content: center; 78 | margin-top: 16px; 79 | } 80 | 81 | .card { 82 | border-radius: 4px; 83 | border: 1px solid #eee; 84 | background-color: #fafafa; 85 | height: 40px; 86 | width: 200px; 87 | margin: 0 8px 16px; 88 | padding: 16px; 89 | display: flex; 90 | flex-direction: row; 91 | justify-content: center; 92 | align-items: center; 93 | transition: all 0.2s ease-in-out; 94 | line-height: 24px; 95 | } 96 | 97 | .card-container .card:not(:last-child) { 98 | margin-right: 0; 99 | } 100 | 101 | .card.card-small { 102 | height: 16px; 103 | width: 168px; 104 | } 105 | 106 | .card-container .card:not(.highlight-card) { 107 | cursor: pointer; 108 | } 109 | 110 | .card-container .card:not(.highlight-card):hover { 111 | transform: translateY(-3px); 112 | box-shadow: 0 4px 17px rgba(black, 0.35); 113 | } 114 | 115 | .card-container .card:not(.highlight-card):hover .material-icons path { 116 | fill: rgb(105, 103, 103); 117 | } 118 | 119 | .card.highlight-card { 120 | background-color: #24292e; 121 | color: white; 122 | font-weight: 600; 123 | border: none; 124 | width: auto; 125 | min-width: 30%; 126 | position: relative; 127 | } 128 | 129 | .card.card.highlight-card span { 130 | margin-left: 60px; 131 | } 132 | 133 | svg#rocket { 134 | width: 80px; 135 | position: absolute; 136 | left: -10px; 137 | top: -24px; 138 | } 139 | 140 | svg#rocket-smoke { 141 | height: 100vh; 142 | position: absolute; 143 | top: 10px; 144 | right: 180px; 145 | z-index: -10; 146 | } 147 | 148 | a, 149 | a:visited, 150 | a:hover { 151 | color: #24292e; 152 | text-decoration: none; 153 | } 154 | 155 | a:hover { 156 | color: #125699; 157 | } 158 | 159 | .terminal { 160 | position: relative; 161 | width: 80%; 162 | max-width: 600px; 163 | border-radius: 6px; 164 | padding-top: 45px; 165 | margin-top: 8px; 166 | overflow: hidden; 167 | background-color: rgb(15, 15, 16); 168 | } 169 | 170 | .terminal::before { 171 | content: "\2022 \2022 \2022"; 172 | position: absolute; 173 | top: 0; 174 | left: 0; 175 | height: 4px; 176 | background: rgb(58, 58, 58); 177 | color: #c2c3c4; 178 | width: 100%; 179 | font-size: 2rem; 180 | line-height: 0; 181 | padding: 14px 0; 182 | text-indent: 4px; 183 | } 184 | 185 | .terminal pre { 186 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 187 | color: white; 188 | padding: 0 1rem 1rem; 189 | margin: 0; 190 | } 191 | 192 | .circle-link { 193 | height: 40px; 194 | width: 40px; 195 | border-radius: 40px; 196 | margin: 8px; 197 | background-color: white; 198 | border: 1px solid #eeeeee; 199 | display: flex; 200 | justify-content: center; 201 | align-items: center; 202 | cursor: pointer; 203 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 204 | transition: 1s ease-out; 205 | } 206 | 207 | .circle-link:hover { 208 | transform: translateY(-0.25rem); 209 | box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2); 210 | } 211 | 212 | footer { 213 | margin-top: 8px; 214 | display: flex; 215 | align-items: center; 216 | line-height: 20px; 217 | } 218 | 219 | footer a { 220 | display: flex; 221 | align-items: center; 222 | } 223 | 224 | .github-star-badge { 225 | color: #24292e; 226 | display: flex; 227 | align-items: center; 228 | font-size: 12px; 229 | padding: 3px 10px; 230 | border: 1px solid rgba(27, 31, 35, 0.2); 231 | border-radius: 3px; 232 | background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%); 233 | margin-left: 4px; 234 | font-weight: 600; 235 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, 236 | sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; 237 | } 238 | 239 | .github-star-badge:hover { 240 | background-image: linear-gradient(-180deg, #f0f3f6, #e6ebf1 90%); 241 | border-color: rgba(27, 31, 35, 0.35); 242 | background-position: -0.5em; 243 | } 244 | 245 | .github-star-badge .material-icons { 246 | height: 16px; 247 | width: 16px; 248 | margin-right: 4px; 249 | } 250 | 251 | svg#clouds { 252 | position: fixed; 253 | bottom: -160px; 254 | left: -230px; 255 | z-index: -10; 256 | width: 1920px; 257 | } 258 | 259 | .avatar { 260 | height: 35px; 261 | width: 35px; 262 | border-radius: 32px; 263 | } 264 | 265 | .text-center { 266 | text-align: center; 267 | } 268 | 269 | /* Responsive Styles */ 270 | @media screen and (max-width: 767px) { 271 | .card-container > *:not(.circle-link), 272 | .terminal { 273 | width: 100%; 274 | } 275 | 276 | .card:not(.highlight-card) { 277 | height: 16px; 278 | margin: 8px 0; 279 | } 280 | 281 | .card.highlight-card span { 282 | margin-left: 72px; 283 | } 284 | 285 | svg#rocket-smoke { 286 | right: 120px; 287 | transform: rotate(-5deg); 288 | } 289 | } 290 | 291 | @media screen and (max-width: 575px) { 292 | svg#rocket-smoke { 293 | display: none; 294 | visibility: hidden; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "es2020", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "align": { 5 | "options": [ 6 | "parameters", 7 | "statements" 8 | ] 9 | }, 10 | "array-type": false, 11 | "arrow-parens": false, 12 | "arrow-return-shorthand": true, 13 | "deprecation": { 14 | "severity": "warning" 15 | }, 16 | "component-class-suffix": true, 17 | "contextual-lifecycle": true, 18 | "curly": true, 19 | "directive-class-suffix": true, 20 | "directive-selector": [ 21 | true, 22 | "attribute", 23 | "app", 24 | "camelCase" 25 | ], 26 | "component-selector": [ 27 | true, 28 | "element", 29 | "app", 30 | "kebab-case" 31 | ], 32 | "eofline": true, 33 | "import-blacklist": [ 34 | true, 35 | "rxjs/Rx" 36 | ], 37 | "import-spacing": true, 38 | "indent": { 39 | "options": [ 40 | "spaces" 41 | ] 42 | }, 43 | "interface-name": false, 44 | "max-classes-per-file": false, 45 | "max-line-length": [ 46 | true, 47 | 140 48 | ], 49 | "member-access": false, 50 | "member-ordering": [ 51 | true, 52 | { 53 | "order": [ 54 | "static-field", 55 | "instance-field", 56 | "static-method", 57 | "instance-method" 58 | ] 59 | } 60 | ], 61 | "no-consecutive-blank-lines": false, 62 | "no-console": [ 63 | true, 64 | "debug", 65 | "info", 66 | "time", 67 | "timeEnd", 68 | "trace" 69 | ], 70 | "no-empty": false, 71 | "no-inferrable-types": [ 72 | true, 73 | "ignore-params" 74 | ], 75 | "no-non-null-assertion": true, 76 | "no-redundant-jsdoc": true, 77 | "no-switch-case-fall-through": true, 78 | "no-var-requires": false, 79 | "object-literal-key-quotes": [ 80 | true, 81 | "as-needed" 82 | ], 83 | "object-literal-sort-keys": false, 84 | "ordered-imports": false, 85 | "quotemark": [ 86 | true, 87 | "single" 88 | ], 89 | "trailing-comma": false, 90 | "no-conflicting-lifecycle": true, 91 | "no-host-metadata-property": true, 92 | "no-input-rename": true, 93 | "no-inputs-metadata-property": true, 94 | "no-output-native": true, 95 | "no-output-on-prefix": true, 96 | "no-output-rename": true, 97 | "semicolon": { 98 | "options": [ 99 | "always" 100 | ] 101 | }, 102 | "space-before-function-paren": { 103 | "options": { 104 | "anonymous": "never", 105 | "asyncArrow": "always", 106 | "constructor": "never", 107 | "method": "never", 108 | "named": "never" 109 | } 110 | }, 111 | "no-outputs-metadata-property": true, 112 | "template-banana-in-box": true, 113 | "template-no-negated-async": true, 114 | "typedef-whitespace": { 115 | "options": [ 116 | { 117 | "call-signature": "nospace", 118 | "index-signature": "nospace", 119 | "parameter": "nospace", 120 | "property-declaration": "nospace", 121 | "variable-declaration": "nospace" 122 | }, 123 | { 124 | "call-signature": "onespace", 125 | "index-signature": "onespace", 126 | "parameter": "onespace", 127 | "property-declaration": "onespace", 128 | "variable-declaration": "onespace" 129 | } 130 | ] 131 | }, 132 | "use-lifecycle-interface": true, 133 | "use-pipe-transform-interface": true, 134 | "variable-name": { 135 | "options": [ 136 | "ban-keywords", 137 | "check-format", 138 | "allow-pascal-case" 139 | ] 140 | }, 141 | "whitespace": { 142 | "options": [ 143 | "check-branch", 144 | "check-decl", 145 | "check-operator", 146 | "check-separator", 147 | "check-type", 148 | "check-typecast" 149 | ] 150 | } 151 | }, 152 | "rulesDirectory": [ 153 | "codelyzer" 154 | ] 155 | } --------------------------------------------------------------------------------