├── .browserslistrc ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── angular.json ├── demo.png ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── projects └── ngx-avatar │ ├── karma.conf.js │ ├── ng-package.json │ ├── ng-package.prod.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── avatar-config.service.spec.ts │ │ ├── avatar-config.service.ts │ │ ├── avatar-config.token.ts │ │ ├── avatar-config.ts │ │ ├── avatar.component.spec.ts │ │ ├── avatar.component.ts │ │ ├── avatar.module.ts │ │ ├── avatar.service.spec.ts │ │ ├── avatar.service.ts │ │ └── sources │ │ │ ├── async-source.ts │ │ │ ├── avatar-source.enum.ts │ │ │ ├── custom.ts │ │ │ ├── facebook.ts │ │ │ ├── github.ts │ │ │ ├── google.ts │ │ │ ├── gravatar.ts │ │ │ ├── initials.ts │ │ │ ├── instagram.ts │ │ │ ├── skype.ts │ │ │ ├── source.creator.ts │ │ │ ├── source.factory.ts │ │ │ ├── source.ts │ │ │ ├── twitter.ts │ │ │ ├── value.ts │ │ │ └── vkontakte.ts │ ├── public_api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ └── tslint.json ├── scripts └── copy-artifacts.js ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── package.json │ ├── user.model.ts │ └── user.service.ts ├── assets │ ├── .gitkeep │ ├── data │ │ └── data.json │ └── img │ │ └── avatar.jpg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #IDE configuration files 2 | .idea 3 | .vscode 4 | 5 | # Dependency directory 6 | node_modules 7 | typings 8 | 9 | #Test 10 | /coverage 11 | 12 | # custom 13 | /dist 14 | .DS_Store 15 | src/.DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12' 4 | sudo: required 5 | dist: trusty 6 | addons: 7 | chrome: stable 8 | before_script: 9 | - "sudo chown root /opt/google/chrome/chrome-sandbox" 10 | - "sudo chmod 4755 /opt/google/chrome/chrome-sandbox" 11 | before_install: 12 | - export CHROME_BIN=chromium-browser 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | - curl -o- -L https://yarnpkg.com/install.sh | bash 16 | - export PATH=$HOME/.yarn/bin:$PATH 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Haithem Mosbahi, ngx-avatar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## <ngx-avatar> 2 | [![npm version](https://badge.fury.io/js/ngx-avatar.svg)](https://badge.fury.io/js/ngx-avatar) [![npm](https://img.shields.io/npm/dt/ngx-avatar.svg)](https://www.npmjs.com/package/ngx-avatar) [![Build Status](https://travis-ci.org/HaithemMosbahi/ngx-avatar.svg?branch=master)](https://travis-ci.org/HaithemMosbahi/ngx-avatar) 3 | [![Angular Style Guide](https://mgechev.github.io/angular2-style-guide/images/badge.svg)](https://angular.io/styleguide) 4 | ![size](https://img.shields.io/bundlephobia/minzip/ngx-avatar.svg) 5 | 6 | A universal avatar component for Angular applications that fetches / generates avatar based on the information you have about the user. The component has a fallback system that if for example an invalid Facebook ID is used it will try google ID and so on. 7 | 8 | You can use this component whether you have a single source or a multiple avatar sources. In this case the fallback system will fetch the first valid avatar. 9 | 10 | Moreover, the component can shows name initials or simple value as avatar. 11 | 12 | ![Angular Avatar component preview](https://cdn.rawgit.com/HaithemMosbahi/ngx-avatar/0bac9072/demo.png) 13 | 14 | 15 | Supported avatar sources: 16 | 17 | * Facebook 18 | * Google 19 | * Twitter 20 | * Instagram 21 | * Vkontakte (VK) 22 | * Skype 23 | * Gravatar 24 | * GitHub 25 | * Custom image 26 | * name initials 27 | * value 28 | 29 | The fallback system uses the same order as the above source list, Facebook has the highest priority, if it fails, google source will be used, and so on. 30 | 31 | If you enjoy watching videos, check out this [tutorial](https://medium.com/letsboot/lets-play-with-ngx-avatar-ec585dc39161) on medium which explains how to use ngx-avatar in your angular application. 32 | 33 | Check out this [link](https://stackblitz.com/edit/ngx-avatar-demo) to play with ngx-avatar :grinning: 34 | 35 | ## Installation 36 | 37 | Install avatar component using [Yarn](https://yarnpkg.com/): 38 | 39 | ```bash 40 | $ yarn add ngx-avatar 41 | ``` 42 | 43 | or 44 | 45 | ```bash 46 | $ npm install ngx-avatar --save 47 | ``` 48 | 49 | ## Usage 50 | 51 | 1. Import AvatarModule: 52 | 53 | Once you have installed ngx-avatar, you can import it in your `AppModule`: 54 | 55 | ```typescript 56 | import { BrowserModule } from '@angular/platform-browser'; 57 | import { NgModule } from '@angular/core'; 58 | import { HttpClientModule } from '@angular/common/http'; 59 | 60 | import { AppComponent } from './app.component'; 61 | 62 | // Import your AvatarModule 63 | import { AvatarModule } from 'ngx-avatar'; 64 | 65 | @NgModule({ 66 | declarations: [ 67 | AppComponent 68 | ], 69 | imports: [ 70 | BrowserModule, 71 | HttpClientModule, 72 | // Specify AvatarModule as an import 73 | AvatarModule 74 | ], 75 | providers: [], 76 | bootstrap: [AppComponent] 77 | }) 78 | export class AppModule { } 79 | ``` 80 | 81 | Starting from version 3.4.0: 82 | - `HttpClientModule` is mandatory in order to fetch the avatar from external sources (Gravatar, Google, ...). 83 | 84 | 2. Start using it: 85 | 86 | Once the AvatarModule is imported, you can start using the component in your Angular application: 87 | 88 | ```html 89 | 90 | ``` 91 | ## Examples 92 | 93 | ```html 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 110 | 111 | 112 | ``` 113 | Check out this [file](https://github.com/HaithemMosbahi/ngx-avatar/blob/master/demo/src/app/app.component.html) for more examples on how to use ngx-avatar in your application. 114 | 115 | ## Demo 116 | Check out this [link](https://ngx-avatar-demo.stackblitz.io/) for a live demo. 117 | Also, you can play with ngx-avatar using an online editor [here](https://stackblitz.com/edit/ngx-avatar-demo) on stackblitz. 118 | 119 | Moreover, the demo folder contains an application generated with angular cli that uses ngx-avatar component. 120 | 121 | To run the demo application : 122 | ```bash 123 | $ yarn install 124 | $ ng serve 125 | ``` 126 | 127 | 128 | 129 | ## Options 130 | 131 | | Attribute | Type | Default | Description | 132 | | ------------- | ---------------- | ------- | ------------------------------------------------------------------------------------------------------ | 133 | | `facebookId` | *string \| null* | | Facebook ID | 134 | | `googleId` | *string \| null* | | Google ID | 135 | | `twitterId` | *string \| null* | | Twitter Handle | 136 | | `instagramId` | *string \| null* | | Instagram Handle | 137 | | `vkontakteId` | *string \| null* | | VK ID | 138 | | `skypeId` | *string \| null* | | Skype ID | 139 | | `gravatarId` | *string \| null* | | email or md5 email related to gravatar | 140 | | `githubId` | *string \| null* | | Github ID | 141 | | `src` | *string \| null* | | Fallback image to use | 142 | | `name` | *string \| null* | | Will be used to generate avatar based on the initials of the person | 143 | | `value` | *string \| null* | | Show a value as avatar | 144 | | `initialsSize`| *number* | 0 | Restricts the size of initials - it goes along with the name property and can be used to fix the number of characters that will be displayed as initials. The `0` means no restrictions. | 145 | | `bgColor` | *string* | random | Give the background a fixed color with a hex like for example #FF0000 | 146 | | `fgColor` | *string* | #FFF | Give the text a fixed color with a hex like for example #FF0000 | 147 | | `size` | *number* | 50 | Size of the avatar | 148 | | `textSizeRatio`| *number* | 3 | For text based avatars the size of the text as a fragment of size (size / textSizeRatio) | 149 | | `round` | *boolean* | true | Round the avatar corners | 150 | | `cornerRadius`| *number* | 0 | Square avatars can have rounded corners using this property | 151 | | `borderColor` | *string* | undefined | Add border with the given color. boder's default style is '1px solid borderColor' | 152 | | `style` | *object* | | Style that will be applied on the root element | 153 | | `clickOnAvatar`| *Output* | | Fired when the avatar is clicked. The component emits the source object that has been used to fetch the avatar.| 154 | 155 | The source object has the following properties: 156 | * sourceType : avatar source ( Facebook, twitter, etc) 157 | * sourceId : identifier of the user 158 | * getAvatar(size) : method to fetch user avatar from the current source 159 | 160 | ## Override Avatar Configuration 161 | The avatar module provides the possibility of customizing the avatar component by overriding some of its options. For example, the avatar module comes with a set of default colors used to randomly fill the background color of the avatar. Thus, it's possible to change the default list of colors and to pass your own list. 162 | 163 | All you need to do is to configure the AvatarModule by calling **forRoot** method. The forRoot method takes an AvatarConfig Object that contains the overridden options. 164 | 165 | AvatarConfig interface has two properties: 166 | * **avatarColors:** allows the user to override the default avatar colors by providing a new set of colors 167 | * **sourcePriorityOrder:** allows the user to change the avatar source priority order. If you want the avatar component to look for user initials first, twitter before facebook or any order you want, this is can be done using the sourcePriorityOrder property 168 | 169 | The following code shows an example on how to import the AvatarModule with your own source priority order. 170 | With the given order, the avatar component will look first for the custom avatar image and then for user initials and after that it will look the rest of sources. 171 | 172 | ```typescript 173 | import { BrowserModule } from '@angular/platform-browser'; 174 | import { NgModule } from '@angular/core'; 175 | import { HttpClientModule } from "@angular/common/http"; 176 | 177 | import { AppComponent } from './app.component'; 178 | import { UserService } from "./user.service"; 179 | import { AvatarModule, AvatarSource } from 'ngx-avatar'; 180 | 181 | const avatarSourcesOrder = [AvatarSource.CUSTOM, AvatarSource.INITIALS]; 182 | 183 | @NgModule({ 184 | declarations: [ 185 | AppComponent ], 186 | imports: [ 187 | BrowserModule, 188 | HttpClientModule, 189 | // import AvatarModule in your app with your own configuration 190 | AvatarModule.forRoot({ 191 | sourcePriorityOrder: avatarSourcesOrder 192 | }) 193 | ], 194 | providers: [UserService], 195 | bootstrap: [AppComponent] 196 | }) 197 | export class AppModule { } 198 | 199 | ``` 200 | 201 | Here's an example on how to import the AvatarModule with your own set of colors. 202 | 203 | * Starting from version 3.1, overriding the avatar configuration can be done as follows: 204 | 205 | ```typescript 206 | import { BrowserModule } from '@angular/platform-browser'; 207 | import { NgModule } from '@angular/core'; 208 | 209 | import { AppComponent } from './app.component'; 210 | import { UserService } from "./user.service"; 211 | import { AvatarModule } from "ngx-avatar"; 212 | import { HttpClientModule } from "@angular/common/http"; 213 | 214 | const avatarColors = ["#FFB6C1", "#2c3e50", "#95a5a6", "#f39c12", "#1abc9c"]; 215 | 216 | @NgModule({ 217 | declarations: [ 218 | AppComponent ], 219 | imports: [ 220 | BrowserModule, 221 | HttpClientModule, 222 | // import AvatarModule in your app with your own configuration 223 | AvatarModule.forRoot({ 224 | colors: avatarColors 225 | }) 226 | ], 227 | providers: [UserService], 228 | bootstrap: [AppComponent] 229 | }) 230 | export class AppModule { } 231 | 232 | ``` 233 | 234 | * Users who use a prior version of ngx-avatar ( < 3.1 ) can override the configuration as follows: 235 | 236 | ```typescript 237 | import { BrowserModule } from '@angular/platform-browser'; 238 | import { NgModule } from '@angular/core'; 239 | 240 | import { AppComponent } from './app.component'; 241 | import { UserService } from "./user.service"; 242 | import { AvatarModule,AvatarConfig } from "ngx-avatar"; 243 | import { HttpClientModule } from "@angular/common/http"; 244 | 245 | const avatarConfig = new AvatarConfig(['red','blue','pink']); 246 | 247 | @NgModule({ 248 | declarations: [ 249 | AppComponent ], 250 | imports: [ 251 | BrowserModule, 252 | HttpClientModule, 253 | // import AvatarModule in your app with your own configuration 254 | AvatarModule.forRoot(avatarConfig) 255 | ], 256 | providers: [UserService], 257 | bootstrap: [AppComponent] 258 | }) 259 | export class AppModule { } 260 | 261 | ``` 262 | 263 | **Avatar Styling** 264 | 265 | In addition to the style attribute, ngx-avatar style can be customized using css classes. Thus, the generated code offers two css classes that can be overridden : 266 | * **avatar-container** : class that represents the avatar container - the host element. Styles in this class will be applied on the avatar whether is an image or text. 267 | * **avatar-content** : css class that represents the avatar element which is embedded inside the avatar-container. 268 | 269 | To overcome Angular's view encapsulation, you may need to use the /deep/ operator to target it. Here's an example that shows how to override ngx-avatar style : 270 | 271 | ```html 272 | 273 | ``` 274 | Your css file might look like this 275 | 276 | ```css 277 | .my-avatar /deep/ .avatar-content { 278 | background-color : red !important; 279 | } 280 | ``` 281 | 282 | ## Release Notes & History 283 | * 4.1.0: Angular 11 support 284 | * 4.0.0: Angular 9 support and minor improvements 285 | * 3.6.0: Angular 8 support 286 | * 3.5.0: export Avatar component for Angular elements and ng upgrade 287 | * 3.4.0: http module is removed from the library dependencies. Applications' http module will be used instead. 288 | * 3.3.x: Bug fixes 289 | * 3.3.0: Override Source priority order when importing AvatarModule 290 | * 3.2.0: Add support to Angular 7 291 | * 3.1.1: fixes the source priority bug 292 | * 3.1: fixes AOT / Prod build when loading avatar module with config 293 | * This version has a **breaking change** in the way the module with configuration is imported, for more details see Override Avatar Configuration section. 294 | * 3.0: Add support to Angular 6 295 | * Build the library with Angular CLI 296 | * 2.9: Bug fixes [#16](https://github.com/HaithemMosbahi/ngx-avatar/issues/16) & [#16](https://github.com/HaithemMosbahi/ngx-avatar/issues/16) 297 | * 2.8: add initials size option 298 | * 2.7: code refactoring 299 | * 2.6: Customize avatar options 300 | * 2.5: Bug fixes & new css classes 301 | * 2.4: Refactor async sources 302 | * 2.3: Add support for github avatar 303 | * 2.2: Fix prod and aot build 304 | * 2.1: Bug fixes 305 | * 2.0: add support to vkontakte source 306 | * 1.4: background color is now generated based on the sum of ASCII values of avatar's text. 307 | * 1.3: Bug Fixes ( support dynamic avatar data ) 308 | * 1.2: Add border related properties. 309 | * 1.1: Listen to click events on avatar and support retina display. 310 | * 1.0: Avatar component that fetches / generates user avatar from different sources. 311 | 312 | 313 | ## Contributing 314 | 315 | Contributions and all possible collaboration are welcome. 316 | 317 | * Fork it! 318 | * Create your feature branch: git checkout -b my-new-feature 319 | * Commit your changes: git commit -am 'Add some feature' 320 | * Push to the branch: git push origin my-new-feature 321 | * Submit a pull request :D 322 | 323 | 324 | # Testing 325 | 326 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.1.1. 327 | 328 | ## Development server 329 | 330 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 331 | 332 | ## Code scaffolding 333 | 334 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 335 | 336 | ## Build 337 | 338 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 339 | 340 | ## Running unit tests 341 | 342 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 343 | 344 | ## Running end-to-end tests 345 | 346 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 347 | 348 | ## Further help 349 | 350 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 351 | 352 | ## License 353 | 354 | MIT © [Haithem Mosbahi](mailto:haithem.mosbahi@gmail.com) 355 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "cli": { 5 | "packageManager": "yarn" 6 | }, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "ngx-avatar-demo": { 10 | "root": "", 11 | "sourceRoot": "src", 12 | "projectType": "application", 13 | "prefix": "app", 14 | "schematics": { 15 | "@schematics/angular:application": { 16 | "strict": true 17 | }, 18 | "@schematics/angular:component": { 19 | "style": "scss", 20 | "skipTests": true 21 | }, 22 | "@schematics/angular:class": { 23 | "skipTests": true 24 | }, 25 | "@schematics/angular:directive": { 26 | "skipTests": true 27 | }, 28 | "@schematics/angular:guard": { 29 | "skipTests": true 30 | }, 31 | "@schematics/angular:module": { 32 | "skipTests": true 33 | }, 34 | "@schematics/angular:pipe": { 35 | "skipTests": true 36 | }, 37 | "@schematics/angular:service": { 38 | "skipTests": true 39 | } 40 | }, 41 | "architect": { 42 | "build": { 43 | "builder": "@angular-devkit/build-angular:browser", 44 | "options": { 45 | "aot": true, 46 | "outputPath": "dist/ngx-avatar-demo", 47 | "index": "src/index.html", 48 | "main": "src/main.ts", 49 | "polyfills": "src/polyfills.ts", 50 | "tsConfig": "src/tsconfig.app.json", 51 | "assets": [ 52 | "src/favicon.ico", 53 | "src/assets" 54 | ], 55 | "styles": [ 56 | "src/styles.scss" 57 | ], 58 | "scripts": [] 59 | }, 60 | "configurations": { 61 | "production": { 62 | "budgets": [ 63 | { 64 | "type": "anyComponentStyle", 65 | "maximumWarning": "6kb" 66 | } 67 | ], 68 | "fileReplacements": [ 69 | { 70 | "replace": "src/environments/environment.ts", 71 | "with": "src/environments/environment.prod.ts" 72 | } 73 | ], 74 | "optimization": true, 75 | "outputHashing": "all", 76 | "sourceMap": false, 77 | "extractCss": true, 78 | "namedChunks": false, 79 | "aot": true, 80 | "extractLicenses": true, 81 | "vendorChunk": false, 82 | "buildOptimizer": true 83 | } 84 | } 85 | }, 86 | "serve": { 87 | "builder": "@angular-devkit/build-angular:dev-server", 88 | "options": { 89 | "browserTarget": "ngx-avatar-demo:build" 90 | }, 91 | "configurations": { 92 | "production": { 93 | "browserTarget": "ngx-avatar-demo:build:production" 94 | } 95 | } 96 | }, 97 | "extract-i18n": { 98 | "builder": "@angular-devkit/build-angular:extract-i18n", 99 | "options": { 100 | "browserTarget": "ngx-avatar-demo:build" 101 | } 102 | }, 103 | "test": { 104 | "builder": "@angular-devkit/build-angular:karma", 105 | "options": { 106 | "main": "src/test.ts", 107 | "polyfills": "src/polyfills.ts", 108 | "tsConfig": "src/tsconfig.spec.json", 109 | "karmaConfig": "src/karma.conf.js", 110 | "styles": [ 111 | "src/styles.scss" 112 | ], 113 | "scripts": [], 114 | "assets": [ 115 | "src/favicon.ico", 116 | "src/assets" 117 | ], 118 | "watch": false 119 | } 120 | }, 121 | "lint": { 122 | "builder": "@angular-devkit/build-angular:tslint", 123 | "options": { 124 | "tsConfig": [ 125 | "src/tsconfig.app.json", 126 | "src/tsconfig.spec.json" 127 | ], 128 | "exclude": [ 129 | "**/node_modules/**" 130 | ] 131 | } 132 | } 133 | } 134 | }, 135 | "ngx-avatar-demo-e2e": { 136 | "root": "e2e/", 137 | "projectType": "application", 138 | "architect": { 139 | "e2e": { 140 | "builder": "@angular-devkit/build-angular:protractor", 141 | "options": { 142 | "protractorConfig": "e2e/protractor.conf.js", 143 | "devServerTarget": "ngx-avatar-demo:serve" 144 | }, 145 | "configurations": { 146 | "production": { 147 | "devServerTarget": "ngx-avatar-demo:serve:production" 148 | } 149 | } 150 | }, 151 | "lint": { 152 | "builder": "@angular-devkit/build-angular:tslint", 153 | "options": { 154 | "tsConfig": "e2e/tsconfig.e2e.json", 155 | "exclude": [ 156 | "**/node_modules/**" 157 | ] 158 | } 159 | } 160 | } 161 | }, 162 | "ngx-avatar": { 163 | "root": "projects/ngx-avatar", 164 | "sourceRoot": "projects/ngx-avatar/src", 165 | "projectType": "library", 166 | "prefix": "lib", 167 | "architect": { 168 | "build": { 169 | "builder": "@angular-devkit/build-ng-packagr:build", 170 | "options": { 171 | "tsConfig": "projects/ngx-avatar/tsconfig.lib.json", 172 | "project": "projects/ngx-avatar/ng-package.json" 173 | }, 174 | "configurations": { 175 | "production": { 176 | "project": "projects/ngx-avatar/ng-package.prod.json" 177 | , "tsConfig": "projects/ngx-avatar/tsconfig.lib.prod.json" 178 | } 179 | } 180 | }, 181 | "test": { 182 | "builder": "@angular-devkit/build-angular:karma", 183 | "options": { 184 | "main": "projects/ngx-avatar/src/test.ts", 185 | "tsConfig": "projects/ngx-avatar/tsconfig.spec.json", 186 | "karmaConfig": "projects/ngx-avatar/karma.conf.js", 187 | "watch": false 188 | } 189 | }, 190 | "lint": { 191 | "builder": "@angular-devkit/build-angular:tslint", 192 | "options": { 193 | "tsConfig": [ 194 | "projects/ngx-avatar/tsconfig.lib.json", 195 | "projects/ngx-avatar/tsconfig.spec.json" 196 | ], 197 | "exclude": [ 198 | "**/node_modules/**" 199 | ] 200 | } 201 | } 202 | } 203 | } 204 | }, 205 | "defaultProject": "ngx-avatar-demo" 206 | } 207 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HaithemMosbahi/ngx-avatar/22e9715e6c6499d22997982b2b46ca3323cbe683/demo.png -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to testing!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing", 3 | "version": "0.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/HaithemMosbahi/ngx-avatar" 7 | }, 8 | "scripts": { 9 | "ng": "ng", 10 | "start": "ng serve", 11 | "build": "ng build", 12 | "postbuild": "node scripts/copy-artifacts.js", 13 | "test": "ng test", 14 | "lint": "ng lint", 15 | "e2e": "ng e2e" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "@angular/animations": "^11.1.0", 20 | "@angular/common": "^11.1.0", 21 | "@angular/compiler": "^11.1.0", 22 | "@angular/core": "^11.1.0", 23 | "@angular/forms": "^11.1.0", 24 | "@angular/platform-browser": "^11.1.0", 25 | "@angular/platform-browser-dynamic": "^11.1.0", 26 | "@angular/router": "^11.1.0", 27 | "rxjs": "^6.6.3", 28 | "ts-md5": "^1.2.7", 29 | "tslib": "^2.1.0", 30 | "zone.js": "~0.11.3" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~0.1101.0", 34 | "@angular-devkit/build-ng-packagr": "~0.1002.0", 35 | "@angular/cli": "~11.1.0", 36 | "@angular/compiler-cli": "^11.1.0", 37 | "@angular/language-service": "^11.1.0", 38 | "@types/jasmine": "~3.6.3", 39 | "@types/jasminewd2": "~2.0.8", 40 | "@types/node": "^14.14.22", 41 | "codelyzer": "^6.0.1", 42 | "jasmine-core": "~3.6.0", 43 | "jasmine-spec-reporter": "~6.0.0", 44 | "karma": "~6.0.1", 45 | "karma-chrome-launcher": "~3.1.0", 46 | "karma-coverage-istanbul-reporter": "~3.0.3", 47 | "karma-jasmine": "~4.0.1", 48 | "karma-jasmine-html-reporter": "^1.5.4", 49 | "karma-mocha-reporter": "^2.2.5", 50 | "ng-packagr": "^11.1.2", 51 | "protractor": "~7.0.0", 52 | "ts-node": "~9.1.1", 53 | "tslint": "~6.1.3", 54 | "typescript": "~4.1.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /projects/ngx-avatar/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 | require('karma-mocha-reporter') 15 | ], 16 | client: { 17 | clearContext: false // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | coverageIstanbulReporter: { 20 | dir: require('path').join(__dirname, '../../coverage'), 21 | reports: ['html', 'lcovonly'], 22 | fixWebpackSourcePaths: true 23 | }, 24 | reporters: ['coverage-istanbul', 'mocha'], 25 | port: 9876, 26 | colors: true, 27 | logLevel: config.LOG_INFO, 28 | autoWatch: true, 29 | browsers: ['Chrome'], 30 | singleRun: false 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/ngx-avatar/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-avatar", 4 | "deleteDestPath": false, 5 | "lib": { 6 | "entryFile": "src/public_api.ts" 7 | }, 8 | "whitelistedNonPeerDependencies": [ 9 | "ts-md5" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /projects/ngx-avatar/ng-package.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-avatar", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | }, 7 | "whitelistedNonPeerDependencies": [ 8 | "ts-md5" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /projects/ngx-avatar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-avatar", 3 | "description": "A universal avatar component for Angular applications that fetches / generates avatar based on the information you have about the user.", 4 | "version": "4.1.0", 5 | "keywords": [ 6 | "angular", 7 | "avatar", 8 | "component", 9 | "initials" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/HaithemMosbahi/ngx-avatar" 15 | }, 16 | "sideEffects": false, 17 | "dependencies": { 18 | "ts-md5": "^1.2.4", 19 | "tslib": "^2.0.0" 20 | }, 21 | "peerDependencies": { 22 | "@angular/common": "^11.1.0", 23 | "@angular/core": "^11.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/avatar-config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { AvatarConfigService } from './avatar-config.service'; 2 | import { AvatarSource } from './sources/avatar-source.enum'; 3 | import { AvatarConfig } from './avatar-config'; 4 | import { defaultSources, defaultColors } from './avatar.service'; 5 | 6 | describe('AvatarConfigService', () => { 7 | describe('AvatarSources', () => { 8 | it('should return the list of sources with the default order when the user provides an empty list of sources', () => { 9 | const userConfig: AvatarConfig = { sourcePriorityOrder: [] }; 10 | const avatarConfigService = new AvatarConfigService(userConfig); 11 | 12 | expect(avatarConfigService.getAvatarSources(defaultSources)).toEqual( 13 | defaultSources 14 | ); 15 | }); 16 | 17 | it('should return the list of sources with the default order when the user does not provide a custom avatar configuration', () => { 18 | const avatarConfigService = new AvatarConfigService({}); 19 | 20 | expect(avatarConfigService.getAvatarSources(defaultSources)).toEqual( 21 | defaultSources 22 | ); 23 | }); 24 | 25 | it('should return the list of sources with the default order when the user provides an unknown list of sources', () => { 26 | const userConfig: AvatarConfig = { 27 | sourcePriorityOrder: ['UNKNOWN_SOURCE' as AvatarSource] 28 | }; 29 | const avatarConfigService = new AvatarConfigService(userConfig); 30 | 31 | expect(avatarConfigService.getAvatarSources(defaultSources)).toEqual( 32 | defaultSources 33 | ); 34 | }); 35 | 36 | it('should override the source priority order when the user provides a valid list of sources', () => { 37 | const userConfig: AvatarConfig = { 38 | sourcePriorityOrder: [AvatarSource.INITIALS, AvatarSource.TWITTER] 39 | }; 40 | const avatarConfigService = new AvatarConfigService(userConfig); 41 | 42 | const expectedSourcesOrder = [ 43 | AvatarSource.INITIALS, 44 | AvatarSource.TWITTER, 45 | AvatarSource.FACEBOOK, 46 | AvatarSource.GOOGLE, 47 | AvatarSource.INSTAGRAM, 48 | AvatarSource.VKONTAKTE, 49 | AvatarSource.SKYPE, 50 | AvatarSource.GRAVATAR, 51 | AvatarSource.GITHUB, 52 | AvatarSource.CUSTOM, 53 | AvatarSource.VALUE 54 | ]; 55 | expect(avatarConfigService.getAvatarSources(defaultSources)).toEqual( 56 | expectedSourcesOrder 57 | ); 58 | }); 59 | 60 | it('should ignore redundant sources', () => { 61 | const userConfig: AvatarConfig = { 62 | sourcePriorityOrder: [AvatarSource.INITIALS, AvatarSource.INITIALS] 63 | }; 64 | const avatarConfigService = new AvatarConfigService(userConfig); 65 | 66 | const expectedSourcesOrder = [ 67 | AvatarSource.INITIALS, 68 | AvatarSource.FACEBOOK, 69 | AvatarSource.GOOGLE, 70 | AvatarSource.TWITTER, 71 | AvatarSource.INSTAGRAM, 72 | AvatarSource.VKONTAKTE, 73 | AvatarSource.SKYPE, 74 | AvatarSource.GRAVATAR, 75 | AvatarSource.GITHUB, 76 | AvatarSource.CUSTOM, 77 | AvatarSource.VALUE 78 | ]; 79 | expect(avatarConfigService.getAvatarSources(defaultSources)).toEqual( 80 | expectedSourcesOrder 81 | ); 82 | }); 83 | }); 84 | 85 | describe('AvatarColors', () => { 86 | it('should return the user\'s list of colors when provided in the avatar configuration', () => { 87 | const userColors = ['#ccc', '#fff']; 88 | const userConfig: AvatarConfig = { 89 | colors: userColors 90 | }; 91 | 92 | const avatarConfigService = new AvatarConfigService(userConfig); 93 | 94 | expect(avatarConfigService.getAvatarColors(defaultColors)).toBe( 95 | userColors 96 | ); 97 | }); 98 | 99 | it('should return the default colors when no colors are provided in the avatar configuration', () => { 100 | const avatarConfigService = new AvatarConfigService({}); 101 | 102 | expect(avatarConfigService.getAvatarColors(defaultColors)).toBe( 103 | defaultColors 104 | ); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/avatar-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, Optional } from '@angular/core'; 2 | 3 | import { AvatarSource } from './sources/avatar-source.enum'; 4 | import { AVATAR_CONFIG } from './avatar-config.token'; 5 | import { AvatarConfig } from './avatar-config'; 6 | 7 | @Injectable() 8 | export class AvatarConfigService { 9 | constructor( 10 | @Optional() 11 | @Inject(AVATAR_CONFIG) 12 | public userConfig: AvatarConfig 13 | ) {} 14 | 15 | public getAvatarSources(defaultSources: AvatarSource[]): AvatarSource[] { 16 | if ( 17 | this.userConfig && 18 | this.userConfig.sourcePriorityOrder && 19 | this.userConfig.sourcePriorityOrder.length 20 | ) { 21 | const uniqueSources = [...new Set(this.userConfig.sourcePriorityOrder)]; 22 | const validSources = uniqueSources.filter(source => 23 | defaultSources.includes(source) 24 | ); 25 | return [ 26 | ...validSources, 27 | ...defaultSources.filter(source => !validSources.includes(source)) 28 | ]; 29 | } 30 | return defaultSources; 31 | } 32 | 33 | public getAvatarColors(defaultColors: string[]): string[] { 34 | return ( 35 | (this.userConfig && 36 | this.userConfig.colors && 37 | this.userConfig.colors.length && 38 | this.userConfig.colors) || 39 | defaultColors 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/avatar-config.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | import { AvatarConfig } from './avatar-config'; 4 | /** 5 | * Token used to inject the AvatarConfig object 6 | */ 7 | export const AVATAR_CONFIG = new InjectionToken('avatar.config'); 8 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/avatar-config.ts: -------------------------------------------------------------------------------- 1 | import { AvatarSource } from './sources/avatar-source.enum'; 2 | 3 | /** 4 | * Represents avatar configuration object. 5 | */ 6 | export interface AvatarConfig { 7 | /** 8 | * The avatars colors. 9 | */ 10 | colors?: string[]; 11 | 12 | /** 13 | * The order in which the avatar sources will be used. 14 | */ 15 | sourcePriorityOrder?: AvatarSource[]; 16 | } 17 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/avatar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { AvatarComponent } from './avatar.component'; 4 | import { SourceFactory } from './sources/source.factory'; 5 | import { AvatarService } from './avatar.service'; 6 | import { By } from '@angular/platform-browser'; 7 | import { SimpleChange } from '@angular/core'; 8 | import { AvatarSource } from './sources/avatar-source.enum'; 9 | import { Observable, of, throwError } from 'rxjs'; 10 | import { Source } from './sources/source'; 11 | 12 | class AvatarServiceMock { 13 | public fetchAvatar(avatarUrl: string): Observable<{ avatar_url: string }> { 14 | return avatarUrl === 'https://api.github.com/users/github-username' ? 15 | of({ 16 | avatar_url: 'https://mocked.url/foo.jpg', 17 | }) : 18 | throwError(new Error('Mocked error for ' + avatarUrl)); 19 | } 20 | 21 | public compareSources(source1: AvatarSource, source2: AvatarSource): number { 22 | return 0; 23 | } 24 | 25 | public isSource(source: string): boolean { 26 | return true; 27 | } 28 | 29 | public isTextAvatar(sourceType: AvatarSource) { 30 | return true; 31 | } 32 | 33 | public getRandomColor(avatarText: string): string { 34 | return ''; 35 | } 36 | 37 | public markSourceAsFailed(source: Source): void { 38 | 39 | } 40 | 41 | public sourceHasFailedBefore(source: Source): boolean { 42 | return source.sourceType === AvatarSource.GRAVATAR; 43 | } 44 | } 45 | 46 | describe('AvatarComponent', () => { 47 | let component: AvatarComponent; 48 | let fixture: ComponentFixture; 49 | let avatarService: AvatarService; 50 | 51 | beforeEach(waitForAsync(() => { 52 | TestBed.configureTestingModule({ 53 | declarations: [AvatarComponent], 54 | providers: [ 55 | SourceFactory, 56 | { provide: AvatarService, useClass: AvatarServiceMock } 57 | ] 58 | }).compileComponents(); 59 | 60 | fixture = TestBed.createComponent(AvatarComponent); 61 | component = fixture.componentInstance; 62 | avatarService = TestBed.inject(AvatarService); 63 | fixture.detectChanges(); 64 | })); 65 | 66 | it('should create', () => { 67 | expect(component).toBeTruthy(); 68 | }); 69 | 70 | describe('AvatarText', () => { 71 | it('should display the initials of the given value', () => { 72 | component.initials = 'John Doe'; 73 | component.ngOnChanges({ 74 | initials: new SimpleChange(null, 'John Doe', true) 75 | }); 76 | 77 | fixture.detectChanges(); 78 | 79 | const avatarTextEl = fixture.debugElement.query( 80 | By.css('.avatar-container > div') 81 | ); 82 | expect(avatarTextEl.nativeElement.textContent.trim()).toBe('JD'); 83 | }); 84 | }); 85 | 86 | it('should not try again failed sources', () => { 87 | component.gravatar = 'invalid@example.com'; 88 | component.initials = 'John Doe'; 89 | component.ngOnChanges({ 90 | gravatar: new SimpleChange(null, 'invalid@example.com', true), 91 | initials: new SimpleChange(null, 'John Doe', true) 92 | }); 93 | 94 | fixture.detectChanges(); 95 | 96 | const avatarTextEl = fixture.debugElement.query( 97 | By.css('.avatar-container > div') 98 | ); 99 | expect(avatarTextEl.nativeElement.textContent.trim()).toBe('JD'); 100 | }); 101 | 102 | it('should try next async source if first async source fails', () => { 103 | spyOn(avatarService, 'isTextAvatar').and.returnValue(false); 104 | component.google = 'invalid@example.com'; 105 | component.github = 'github-username'; 106 | component.ngOnChanges({ 107 | google: new SimpleChange(null, 'invalid@example.com', true), 108 | github: new SimpleChange(null, 'github-username', true) 109 | }); 110 | 111 | fixture.detectChanges(); 112 | 113 | const avatarImgEl = fixture.debugElement.query( 114 | By.css('.avatar-container > img') 115 | ); 116 | expect(avatarImgEl.nativeElement.src).toBe('https://mocked.url/foo.jpg&s=50'); 117 | }); 118 | 119 | describe('AvatarImage', () => {}); 120 | }); 121 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/avatar.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | Output, 5 | EventEmitter, 6 | OnChanges, 7 | SimpleChanges, 8 | OnDestroy 9 | } from '@angular/core'; 10 | 11 | import { Source } from './sources/source'; 12 | import { AsyncSource } from './sources/async-source'; 13 | import { SourceFactory } from './sources/source.factory'; 14 | import { AvatarService } from './avatar.service'; 15 | import { AvatarSource } from './sources/avatar-source.enum'; 16 | import { takeWhile, map } from 'rxjs/operators'; 17 | 18 | type Style = Partial; 19 | 20 | /** 21 | * Universal avatar component that 22 | * generates avatar from different sources 23 | * 24 | * export 25 | * class AvatarComponent 26 | * implements {OnChanges} 27 | */ 28 | 29 | @Component({ 30 | // tslint:disable-next-line:component-selector 31 | selector: 'ngx-avatar', 32 | styles: [ 33 | ` 34 | :host { 35 | border-radius: 50%; 36 | } 37 | ` 38 | ], 39 | template: ` 40 |
45 | 55 | 56 |
57 | {{ avatarText }} 58 |
59 |
60 |
61 | ` 62 | }) 63 | export class AvatarComponent implements OnChanges, OnDestroy { 64 | @Input() 65 | public round = true; 66 | @Input() 67 | public size: string | number = 50; 68 | @Input() 69 | public textSizeRatio = 3; 70 | @Input() 71 | public bgColor: string | undefined; 72 | @Input() 73 | public fgColor = '#FFF'; 74 | @Input() 75 | public borderColor: string | undefined; 76 | @Input() 77 | public style: Style = {}; 78 | @Input() 79 | public cornerRadius: string | number = 0; 80 | @Input('facebookId') 81 | public facebook?: string | null; 82 | @Input('twitterId') 83 | public twitter?: string | null; 84 | @Input('googleId') 85 | public google?: string | null; 86 | @Input('instagramId') 87 | public instagram?: string | null; 88 | @Input('vkontakteId') 89 | public vkontakte?: string | null; 90 | @Input('skypeId') 91 | public skype?: string | null; 92 | @Input('gravatarId') 93 | public gravatar?: string | null; 94 | @Input('githubId') 95 | public github?: string | null; 96 | @Input('src') 97 | public custom?: string | null; 98 | @Input('name') 99 | public initials?: string | null; 100 | @Input() 101 | public value?: string | null; 102 | @Input() 103 | public placeholder?: string; 104 | @Input() 105 | public initialsSize: string | number = 0; 106 | 107 | @Output() 108 | public clickOnAvatar: EventEmitter = new EventEmitter(); 109 | 110 | public isAlive = true; 111 | public avatarSrc: string | null = null; 112 | public avatarText: string | null = null; 113 | public avatarStyle: Style = {}; 114 | public hostStyle: Style = {}; 115 | 116 | private currentIndex = -1; 117 | private sources: Source[] = []; 118 | 119 | constructor( 120 | public sourceFactory: SourceFactory, 121 | private avatarService: AvatarService 122 | ) {} 123 | 124 | public onAvatarClicked(): void { 125 | this.clickOnAvatar.emit(this.sources[this.currentIndex]); 126 | } 127 | 128 | /** 129 | * Detect inputs change 130 | * 131 | * param {{ [propKey: string]: SimpleChange }} changes 132 | * 133 | * memberof AvatarComponent 134 | */ 135 | public ngOnChanges(changes: SimpleChanges): void { 136 | for (const propName in changes) { 137 | if (this.avatarService.isSource(propName)) { 138 | const sourceType: AvatarSource = AvatarSource[propName.toUpperCase() as keyof typeof AvatarSource] ; 139 | const currentValue = changes[propName].currentValue; 140 | if (currentValue && typeof currentValue === 'string') { 141 | this.addSource(sourceType, currentValue); 142 | } else { 143 | this.removeSource(sourceType); 144 | } 145 | } 146 | } 147 | // reinitialize the avatar component when a source property value has changed 148 | // the fallback system must be re-invoked with the new values. 149 | this.initializeAvatar(); 150 | } 151 | 152 | /** 153 | * Fetch avatar source 154 | * 155 | * memberOf AvatarComponent 156 | */ 157 | public fetchAvatarSource(): void { 158 | const previousSource = this.sources[this.currentIndex]; 159 | if (previousSource) { 160 | this.avatarService.markSourceAsFailed(previousSource); 161 | } 162 | 163 | const source = this.findNextSource(); 164 | if (!source) { 165 | return; 166 | } 167 | 168 | if (this.avatarService.isTextAvatar(source.sourceType)) { 169 | this.buildTextAvatar(source); 170 | this.avatarSrc = null; 171 | } else { 172 | this.buildImageAvatar(source); 173 | } 174 | } 175 | 176 | private findNextSource(): Source | null { 177 | while (++this.currentIndex < this.sources.length) { 178 | const source = this.sources[this.currentIndex]; 179 | if (source && !this.avatarService.sourceHasFailedBefore(source)) { 180 | return source; 181 | } 182 | } 183 | 184 | return null; 185 | } 186 | 187 | public ngOnDestroy(): void { 188 | this.isAlive = false; 189 | } 190 | 191 | /** 192 | * Initialize the avatar component and its fallback system 193 | */ 194 | private initializeAvatar(): void { 195 | this.currentIndex = -1; 196 | if (this.sources.length > 0) { 197 | this.sortAvatarSources(); 198 | this.fetchAvatarSource(); 199 | this.hostStyle = { 200 | width: this.size + 'px', 201 | height: this.size + 'px' 202 | }; 203 | } 204 | } 205 | 206 | private sortAvatarSources(): void { 207 | this.sources.sort((source1, source2) => 208 | this.avatarService.compareSources(source1.sourceType, source2.sourceType) 209 | ); 210 | } 211 | 212 | private buildTextAvatar(avatarSource: Source): void { 213 | this.avatarText = avatarSource.getAvatar(+this.initialsSize); 214 | this.avatarStyle = this.getInitialsStyle(avatarSource.sourceId); 215 | } 216 | 217 | private buildImageAvatar(avatarSource: Source): void { 218 | this.avatarStyle = this.getImageStyle(); 219 | if (avatarSource instanceof AsyncSource) { 220 | this.fetchAndProcessAsyncAvatar(avatarSource); 221 | } else { 222 | this.avatarSrc = avatarSource.getAvatar(+this.size); 223 | } 224 | } 225 | 226 | /** 227 | * 228 | * returns initials style 229 | * 230 | * memberOf AvatarComponent 231 | */ 232 | private getInitialsStyle(avatarValue: string): Style { 233 | return { 234 | textAlign: 'center', 235 | borderRadius: this.round ? '100%' : this.cornerRadius + 'px', 236 | border: this.borderColor ? '1px solid ' + this.borderColor : '', 237 | textTransform: 'uppercase', 238 | color: this.fgColor, 239 | backgroundColor: this.bgColor 240 | ? this.bgColor 241 | : this.avatarService.getRandomColor(avatarValue), 242 | font: 243 | Math.floor(+this.size / this.textSizeRatio) + 244 | 'px Helvetica, Arial, sans-serif', 245 | lineHeight: this.size + 'px', 246 | ...this.style 247 | }; 248 | } 249 | 250 | /** 251 | * 252 | * returns image style 253 | * 254 | * memberOf AvatarComponent 255 | */ 256 | private getImageStyle(): Style { 257 | return { 258 | maxWidth: '100%', 259 | borderRadius: this.round ? '50%' : this.cornerRadius + 'px', 260 | border: this.borderColor ? '1px solid ' + this.borderColor : '', 261 | width: this.size + 'px', 262 | height: this.size + 'px', 263 | ...this.style, 264 | }; 265 | } 266 | /** 267 | * Fetch avatar image asynchronously. 268 | * 269 | * param {Source} source represents avatar source 270 | * memberof AvatarComponent 271 | */ 272 | private fetchAndProcessAsyncAvatar(source: AsyncSource): void { 273 | if (this.avatarService.sourceHasFailedBefore(source)) { 274 | return; 275 | } 276 | 277 | this.avatarService 278 | .fetchAvatar(source.getAvatar(+this.size)) 279 | .pipe( 280 | takeWhile(() => this.isAlive), 281 | map(response => source.processResponse(response, +this.size)), 282 | ) 283 | .subscribe( 284 | avatarSrc => (this.avatarSrc = avatarSrc), 285 | err => { 286 | this.fetchAvatarSource(); 287 | }, 288 | ); 289 | } 290 | 291 | /** 292 | * Add avatar source 293 | * 294 | * param sourceType avatar source type e.g facebook,twitter, etc. 295 | * param sourceValue source value e.g facebookId value, etc. 296 | */ 297 | private addSource(sourceType: AvatarSource, sourceValue: string): void { 298 | const source = this.sources.find(s => s.sourceType === sourceType); 299 | if (source) { 300 | source.sourceId = sourceValue; 301 | } else { 302 | this.sources.push( 303 | this.sourceFactory.newInstance(sourceType, sourceValue), 304 | ); 305 | } 306 | } 307 | 308 | /** 309 | * Remove avatar source 310 | * 311 | * param sourceType avatar source type e.g facebook,twitter, etc. 312 | */ 313 | private removeSource(sourceType: AvatarSource): void { 314 | this.sources = this.sources.filter(source => source.sourceType !== sourceType); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/avatar.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { AvatarComponent } from './avatar.component'; 5 | import { SourceFactory } from './sources/source.factory'; 6 | import { AvatarService } from './avatar.service'; 7 | import { AvatarConfig } from './avatar-config'; 8 | import { AVATAR_CONFIG } from './avatar-config.token'; 9 | import { AvatarConfigService } from './avatar-config.service'; 10 | 11 | @NgModule({ 12 | imports: [CommonModule], 13 | declarations: [AvatarComponent], 14 | providers: [SourceFactory, AvatarService, AvatarConfigService], 15 | exports: [AvatarComponent] 16 | }) 17 | export class AvatarModule { 18 | static forRoot(avatarConfig?: AvatarConfig): ModuleWithProviders { 19 | return { 20 | ngModule: AvatarModule, 21 | providers: [ 22 | { provide: AVATAR_CONFIG, useValue: avatarConfig ? avatarConfig : {} } 23 | ] 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/avatar.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { 3 | HttpClientTestingModule, 4 | HttpTestingController 5 | } from '@angular/common/http/testing'; 6 | 7 | import { AvatarService, defaultSources, defaultColors } from './avatar.service'; 8 | import { AvatarSource } from './sources/avatar-source.enum'; 9 | import { AvatarConfigService } from './avatar-config.service'; 10 | import { Gravatar } from './sources/gravatar'; 11 | 12 | const avatarServiceConfigSpy = { 13 | getAvatarSources: jasmine 14 | .createSpy('avatarConfigService.getAvatarSources') 15 | .and.returnValue(defaultSources), 16 | getAvatarColors: jasmine 17 | .createSpy('avatarConfigService.getAvatarColors') 18 | .and.returnValue(defaultColors) 19 | }; 20 | 21 | describe('AvatarService', () => { 22 | let avatarService: AvatarService; 23 | let httpMock: HttpTestingController; 24 | 25 | describe('Avatar service with default configuration', () => { 26 | beforeEach(() => { 27 | TestBed.configureTestingModule({ 28 | imports: [HttpClientTestingModule], 29 | providers: [ 30 | AvatarService, 31 | { provide: AvatarConfigService, useValue: avatarServiceConfigSpy } 32 | ] 33 | }); 34 | 35 | avatarService = TestBed.inject(AvatarService); 36 | httpMock = TestBed.inject(HttpTestingController); 37 | }); 38 | 39 | afterEach(() => { 40 | httpMock.verify(); 41 | }); 42 | 43 | it('should be created', () => { 44 | expect(avatarService).toBeTruthy(); 45 | }); 46 | 47 | describe('fetchAvatar', () => { 48 | it('should send get request and fetch avatar data from the given url', () => { 49 | const avatarUrl = 'dummy-avatar-url'; 50 | const expectedAvatarData = { 51 | img: 'url-for-avatar-img' 52 | }; 53 | avatarService.fetchAvatar(avatarUrl).subscribe(avatarData => { 54 | expect(avatarData).toEqual(expectedAvatarData); 55 | }); 56 | 57 | const req = httpMock.expectOne( 58 | request => request.method === 'GET' && request.url === avatarUrl 59 | ); 60 | req.flush(expectedAvatarData); 61 | }); 62 | }); 63 | 64 | describe('isSource', () => { 65 | it('should return true when the given value is a valid avatar source', () => { 66 | const isValidAvatar = avatarService.isSource(AvatarSource.GITHUB); 67 | 68 | expect(isValidAvatar).toBeTruthy(); 69 | }); 70 | 71 | it('should return false when the given value is not a valid avatar source', () => { 72 | const isValidAvatar = avatarService.isSource('unknown-source'); 73 | 74 | expect(isValidAvatar).toBeFalsy(); 75 | }); 76 | }); 77 | 78 | describe('isTextAvatar', () => { 79 | it('should return true when the given value is a text avatar', () => { 80 | expect(avatarService.isTextAvatar(AvatarSource.INITIALS)).toBeTruthy(); 81 | }); 82 | 83 | it('should return false when the given value is not a text avatar', () => { 84 | expect(avatarService.isTextAvatar(AvatarSource.GITHUB)).toBeFalsy(); 85 | }); 86 | }); 87 | 88 | describe('getRandomColor', () => { 89 | it('should return transparent when the given value is undefined', () => { 90 | const color = avatarService.getRandomColor(''); 91 | 92 | expect(color).toBe('transparent'); 93 | }); 94 | 95 | it('should return a random color based on the ascii code of the given value is', () => { 96 | const color = avatarService.getRandomColor('random name'); 97 | const cssColorRegex = /#([a-f]|[A-F]|[0-9]){3}(([a-f]|[A-F]|[0-9]){3})?\b/; 98 | expect(color).toMatch(cssColorRegex); 99 | }); 100 | 101 | it('should not return the same color for two different values', () => { 102 | const color1 = avatarService.getRandomColor('name1'); 103 | const color2 = avatarService.getRandomColor('name2'); 104 | 105 | expect(color1).not.toBe(color2); 106 | }); 107 | }); 108 | 109 | describe('compareSources', () => { 110 | it('should return a negative value when the first avatar type comes after the second one', () => { 111 | expect( 112 | avatarService.compareSources( 113 | AvatarSource.FACEBOOK, 114 | AvatarSource.GOOGLE 115 | ) 116 | ).toBeLessThan(0); 117 | }); 118 | 119 | it('should return a positive value when the first avatar type comes before the second one', () => { 120 | expect( 121 | avatarService.compareSources( 122 | AvatarSource.INITIALS, 123 | AvatarSource.FACEBOOK 124 | ) 125 | ).toBeGreaterThan(0); 126 | }); 127 | 128 | it('should return a zero value when the two give values are equal', () => { 129 | expect( 130 | avatarService.compareSources(AvatarSource.GITHUB, AvatarSource.GITHUB) 131 | ).toBe(0); 132 | }); 133 | 134 | it('should be able to tell if a source has failed before', () => { 135 | const source1 = new Gravatar('source1'); 136 | const source1bis = new Gravatar('source1'); 137 | const source2 = new Gravatar('source2'); 138 | 139 | // At first nothing has failed 140 | expect(avatarService.sourceHasFailedBefore(source1)).toBe(false); 141 | expect(avatarService.sourceHasFailedBefore(source1bis)).toBe(false); 142 | expect(avatarService.sourceHasFailedBefore(source2)).toBe(false); 143 | 144 | avatarService.markSourceAsFailed(source1); 145 | 146 | // source1 has failed, and source1bis should also be considered failed so 147 | // we don't load the same avatar with failure from two component instances. 148 | // source2 is still not failed, even though it is the same type of avatar 149 | expect(avatarService.sourceHasFailedBefore(source1)).toBe(true); 150 | expect(avatarService.sourceHasFailedBefore(source1bis)).toBe(true); 151 | expect(avatarService.sourceHasFailedBefore(source2)).toBe(false); 152 | }); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/avatar.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | 4 | import { Observable } from 'rxjs'; 5 | 6 | import { AvatarConfigService } from './avatar-config.service'; 7 | import { AvatarSource } from './sources/avatar-source.enum'; 8 | import { Source } from './sources/source'; 9 | 10 | /** 11 | * list of Supported avatar sources 12 | */ 13 | export const defaultSources = [ 14 | AvatarSource.FACEBOOK, 15 | AvatarSource.GOOGLE, 16 | AvatarSource.TWITTER, 17 | AvatarSource.INSTAGRAM, 18 | AvatarSource.VKONTAKTE, 19 | AvatarSource.SKYPE, 20 | AvatarSource.GRAVATAR, 21 | AvatarSource.GITHUB, 22 | AvatarSource.CUSTOM, 23 | AvatarSource.INITIALS, 24 | AvatarSource.VALUE 25 | ]; 26 | 27 | /** 28 | * list of default colors 29 | */ 30 | export const defaultColors = [ 31 | '#1abc9c', 32 | '#3498db', 33 | '#f1c40f', 34 | '#8e44ad', 35 | '#e74c3c', 36 | '#d35400', 37 | '#2c3e50', 38 | '#7f8c8d' 39 | ]; 40 | 41 | /** 42 | * Provides utilities methods related to Avatar component 43 | */ 44 | @Injectable() 45 | export class AvatarService { 46 | public avatarSources: AvatarSource[] = defaultSources; 47 | public avatarColors: string[] = defaultColors; 48 | 49 | private readonly failedSources = new Map(); 50 | 51 | constructor( 52 | private http: HttpClient, 53 | private avatarConfigService: AvatarConfigService 54 | ) { 55 | this.overrideAvatarSources(); 56 | this.overrideAvatarColors(); 57 | } 58 | 59 | public fetchAvatar(avatarUrl: string): Observable { 60 | return this.http.get(avatarUrl); 61 | } 62 | 63 | public getRandomColor(avatarText: string): string { 64 | if (!avatarText) { 65 | return 'transparent'; 66 | } 67 | const asciiCodeSum = this.calculateAsciiCode(avatarText); 68 | return this.avatarColors[asciiCodeSum % this.avatarColors.length]; 69 | } 70 | 71 | public compareSources( 72 | sourceType1: AvatarSource, 73 | sourceType2: AvatarSource 74 | ): number { 75 | return ( 76 | this.getSourcePriority(sourceType1) - this.getSourcePriority(sourceType2) 77 | ); 78 | } 79 | 80 | public isSource(source: string): boolean { 81 | return this.avatarSources.includes(source as AvatarSource); 82 | } 83 | 84 | public isTextAvatar(sourceType: AvatarSource): boolean { 85 | return [AvatarSource.INITIALS, AvatarSource.VALUE].includes(sourceType); 86 | } 87 | 88 | private buildSourceKey(source: Source): string { 89 | return source.sourceType + '-' + source.sourceId; 90 | } 91 | 92 | public sourceHasFailedBefore(source: Source): boolean { 93 | return this.failedSources.has(this.buildSourceKey(source)); 94 | } 95 | 96 | public markSourceAsFailed(source: Source): void { 97 | this.failedSources.set(this.buildSourceKey(source), source); 98 | } 99 | 100 | private overrideAvatarSources(): void { 101 | this.avatarSources = this.avatarConfigService.getAvatarSources( 102 | defaultSources 103 | ); 104 | } 105 | 106 | private overrideAvatarColors(): void { 107 | this.avatarColors = this.avatarConfigService.getAvatarColors(defaultColors); 108 | } 109 | 110 | private calculateAsciiCode(value: string): number { 111 | return value 112 | .split('') 113 | .map(letter => letter.charCodeAt(0)) 114 | .reduce((previous, current) => previous + current); 115 | } 116 | 117 | private getSourcePriority(sourceType: AvatarSource) { 118 | return this.avatarSources.indexOf(sourceType); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/async-source.ts: -------------------------------------------------------------------------------- 1 | import { Source } from './source'; 2 | import { AvatarSource } from './avatar-source.enum'; 3 | 4 | /** 5 | * Contract of all async sources. 6 | * Every async source must implement the processResponse method that extracts the avatar url from the data 7 | */ 8 | export abstract class AsyncSource implements Source { 9 | readonly abstract sourceType: AvatarSource; 10 | 11 | constructor(public sourceId: string) {} 12 | 13 | abstract getAvatar(size: number): string; 14 | abstract processResponse(data: unknown, size?: number): string | null; 15 | } 16 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/avatar-source.enum.ts: -------------------------------------------------------------------------------- 1 | export enum AvatarSource { 2 | FACEBOOK = 'facebook', 3 | GOOGLE = 'google', 4 | TWITTER = 'twitter', 5 | INSTAGRAM = 'instagram', 6 | VKONTAKTE = 'vkontakte', 7 | SKYPE = 'skype', 8 | GRAVATAR = 'gravatar', 9 | GITHUB = 'github', 10 | CUSTOM = 'custom', 11 | INITIALS = 'initials', 12 | VALUE = 'value' 13 | } 14 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/custom.ts: -------------------------------------------------------------------------------- 1 | import { Source } from './source'; 2 | import { AvatarSource } from './avatar-source.enum'; 3 | /** 4 | * Custom source implementation. 5 | * return custom image as an avatar 6 | * 7 | */ 8 | export class Custom implements Source { 9 | readonly sourceType: AvatarSource = AvatarSource.CUSTOM; 10 | 11 | constructor(public sourceId: string) {} 12 | 13 | public getAvatar(): string { 14 | return this.sourceId; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/facebook.ts: -------------------------------------------------------------------------------- 1 | import { Source } from './source'; 2 | import { AvatarSource } from './avatar-source.enum'; 3 | /** 4 | * Facebook source implementation. 5 | * Fetch avatar source based on facebook identifier 6 | * and image size 7 | */ 8 | export class Facebook implements Source { 9 | readonly sourceType: AvatarSource = AvatarSource.FACEBOOK; 10 | 11 | constructor(public sourceId: string) {} 12 | 13 | public getAvatar(size: number): string { 14 | return ( 15 | 'https://graph.facebook.com/' + 16 | `${this.sourceId}/picture?width=${size}&height=${size}` 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/github.ts: -------------------------------------------------------------------------------- 1 | import { AsyncSource } from './async-source'; 2 | import { AvatarSource } from './avatar-source.enum'; 3 | 4 | /** 5 | * GitHub source implementation. 6 | * Fetch avatar source based on github identifier 7 | */ 8 | export class Github extends AsyncSource { 9 | readonly sourceType: AvatarSource = AvatarSource.GITHUB; 10 | 11 | constructor(sourceId: string) { 12 | super(sourceId); 13 | } 14 | 15 | public getAvatar(): string { 16 | return `https://api.github.com/users/${this.sourceId}`; 17 | } 18 | 19 | /** 20 | * extract github avatar from json data 21 | */ 22 | public processResponse(data: { avatar_url: string }, size?: number): string { 23 | if (size) { 24 | return `${data.avatar_url}&s=${size}`; 25 | } 26 | return data.avatar_url; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/google.ts: -------------------------------------------------------------------------------- 1 | import { AsyncSource } from './async-source'; 2 | import { AvatarSource } from './avatar-source.enum'; 3 | 4 | /** 5 | * Google source implementation. 6 | * Fetch avatar source based on google identifier 7 | * and image size 8 | */ 9 | export class Google extends AsyncSource { 10 | readonly sourceType: AvatarSource = AvatarSource.GOOGLE; 11 | 12 | constructor(sourceId: string) { 13 | super(sourceId); 14 | } 15 | 16 | public getAvatar(): string { 17 | return `https://picasaweb.google.com/data/entry/api/user/${ 18 | this.sourceId 19 | }?alt=json`; 20 | } 21 | 22 | /** 23 | * Extract google avatar from json data 24 | */ 25 | public processResponse(data: { entry: { gphoto$thumbnail: { $t: string } } }, size?: number): string | null { 26 | const avatarSrc = data.entry.gphoto$thumbnail.$t; 27 | if (avatarSrc) { 28 | return avatarSrc.replace('s64', 's' + size); 29 | } 30 | 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/gravatar.ts: -------------------------------------------------------------------------------- 1 | import { Md5 } from 'ts-md5'; 2 | import { Source } from './source'; 3 | import { AvatarSource } from './avatar-source.enum'; 4 | 5 | function isRetina(): boolean { 6 | if (typeof window !== 'undefined' && window !== null) { 7 | if (window.devicePixelRatio > 1.25) { 8 | return true; 9 | } 10 | 11 | const mediaQuery = '(-webkit-min-device-pixel-ratio: 1.25), (min--moz-device-pixel-ratio: 1.25), (-o-min-device-pixel-ratio: 5/4), (min-resolution: 1.25dppx)'; 12 | if (window.matchMedia && window.matchMedia(mediaQuery).matches) { 13 | return true; 14 | } 15 | } 16 | 17 | return false; 18 | } 19 | 20 | /** 21 | * Gravatar source implementation. 22 | * Fetch avatar source based on gravatar email 23 | */ 24 | export class Gravatar implements Source { 25 | readonly sourceType: AvatarSource = AvatarSource.GRAVATAR; 26 | public sourceId: string; 27 | 28 | constructor(public value: string) { 29 | this.sourceId = value.match('^[a-f0-9]{32}$') 30 | ? value 31 | : Md5.hashStr(value).toString(); 32 | } 33 | 34 | public getAvatar(size: number): string { 35 | const avatarSize = isRetina() ? size * 2 : size; 36 | return `https://secure.gravatar.com/avatar/${ 37 | this.sourceId 38 | }?s=${avatarSize}&d=404`; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/initials.ts: -------------------------------------------------------------------------------- 1 | import { Source } from './source'; 2 | import { AvatarSource } from './avatar-source.enum'; 3 | 4 | /** 5 | * Initials source implementation. 6 | * return the initials of the given value 7 | */ 8 | export class Initials implements Source { 9 | readonly sourceType: AvatarSource = AvatarSource.INITIALS; 10 | 11 | constructor(public sourceId: string) {} 12 | 13 | public getAvatar(size: number): string { 14 | return this.getInitials(this.sourceId, size); 15 | } 16 | 17 | /** 18 | * Returns the initial letters of a name in a string. 19 | */ 20 | private getInitials(name: string, size: number): string { 21 | name = name.trim(); 22 | 23 | if (!name) { 24 | return ''; 25 | } 26 | 27 | const initials = name.split(' '); 28 | 29 | if (size && size < initials.length) { 30 | return this.constructInitials(initials.slice(0, size)); 31 | } else { 32 | return this.constructInitials(initials); 33 | } 34 | } 35 | 36 | /** 37 | * Iterates a person's name string to get the initials of each word in uppercase. 38 | */ 39 | private constructInitials(elements: string[]): string { 40 | if (!elements || !elements.length) { 41 | return ''; 42 | } 43 | 44 | return elements 45 | .filter(element => element && element.length > 0) 46 | .map(element => element[0].toUpperCase()) 47 | .join(''); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/instagram.ts: -------------------------------------------------------------------------------- 1 | import { AsyncSource } from './async-source'; 2 | import { AvatarSource } from './avatar-source.enum'; 3 | 4 | /** 5 | * Instagram source impelementation. 6 | * Fetch avatar source based on instagram identifier 7 | */ 8 | export class Instagram extends AsyncSource { 9 | readonly sourceType: AvatarSource = AvatarSource.INSTAGRAM; 10 | 11 | constructor(sourceId: string) { 12 | super(sourceId); 13 | } 14 | 15 | public getAvatar(): string { 16 | return `https://www.instagram.com/${this.sourceId}/?__a=1`; 17 | } 18 | 19 | /** 20 | * extract instagram avatar from json data 21 | */ 22 | public processResponse(data: { graphql: { user: { profile_pic_url_hd: string } } }, size?: number): string { 23 | return `${data.graphql.user.profile_pic_url_hd}&s=${size}`; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/skype.ts: -------------------------------------------------------------------------------- 1 | import { Source } from './source'; 2 | import { AvatarSource } from './avatar-source.enum'; 3 | /** 4 | * Skype source implementation. 5 | * Fetch avatar source based on skype identifier 6 | */ 7 | export class Skype implements Source { 8 | readonly sourceType: AvatarSource = AvatarSource.SKYPE; 9 | 10 | constructor(public sourceId: string) {} 11 | 12 | public getAvatar(): string { 13 | return `https://api.skype.com/users/${this.sourceId}/profile/avatar`; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/source.creator.ts: -------------------------------------------------------------------------------- 1 | import { Source } from './source'; 2 | 3 | /** 4 | * A creator interface used to instantiate source implementation 5 | */ 6 | export type SourceCreator = new (sourceValue: string) => Source; 7 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/source.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Source } from './source'; 3 | import { Facebook } from './facebook'; 4 | import { Twitter } from './twitter'; 5 | import { Google } from './google'; 6 | import { Instagram } from './instagram'; 7 | import { Custom } from './custom'; 8 | import { Initials } from './initials'; 9 | import { Gravatar } from './gravatar'; 10 | import { Skype } from './skype'; 11 | import { Value } from './value'; 12 | import { Vkontakte } from './vkontakte'; 13 | import { Github } from './github'; 14 | import { SourceCreator } from './source.creator'; 15 | import { AvatarSource } from './avatar-source.enum'; 16 | 17 | /** 18 | * Factory class that implements factory method pattern. 19 | * Used to create Source implementation class based 20 | * on the source Type 21 | */ 22 | @Injectable() 23 | export class SourceFactory { 24 | private sources: { [key: string]: SourceCreator } = {}; 25 | 26 | constructor() { 27 | this.sources[AvatarSource.FACEBOOK] = Facebook; 28 | this.sources[AvatarSource.TWITTER] = Twitter; 29 | this.sources[AvatarSource.GOOGLE] = Google; 30 | this.sources[AvatarSource.INSTAGRAM] = Instagram; 31 | this.sources[AvatarSource.SKYPE] = Skype; 32 | this.sources[AvatarSource.GRAVATAR] = Gravatar; 33 | this.sources[AvatarSource.CUSTOM] = Custom; 34 | this.sources[AvatarSource.INITIALS] = Initials; 35 | this.sources[AvatarSource.VALUE] = Value; 36 | this.sources[AvatarSource.VKONTAKTE] = Vkontakte; 37 | this.sources[AvatarSource.GITHUB] = Github; 38 | } 39 | 40 | public newInstance(sourceType: AvatarSource, sourceValue: string): Source { 41 | return new this.sources[sourceType](sourceValue); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/source.ts: -------------------------------------------------------------------------------- 1 | import { AvatarSource } from './avatar-source.enum'; 2 | 3 | /** 4 | * Contract of all Sources. 5 | * Every source must implements the fetch method 6 | * in order to provide the avatar source. 7 | */ 8 | export interface Source { 9 | /** 10 | * The avatar source type (Facebook, Twitter, etc.). 11 | */ 12 | readonly sourceType: AvatarSource; 13 | 14 | /** 15 | * The avatar id in which it's source recognizes it. 16 | */ 17 | sourceId: string; 18 | 19 | /** 20 | * Gets the avatar that usually is a URL, but, 21 | * for example it can also be a string of initials from the name. 22 | */ 23 | getAvatar(size: number): string; 24 | } 25 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/twitter.ts: -------------------------------------------------------------------------------- 1 | import { Source } from './source'; 2 | import { AvatarSource } from './avatar-source.enum'; 3 | 4 | /** 5 | * Twitter source implementation. 6 | * Fetch avatar source based on google identifier 7 | * and image size 8 | */ 9 | export class Twitter implements Source { 10 | readonly sourceType: AvatarSource = AvatarSource.TWITTER; 11 | 12 | constructor(public sourceId: string) {} 13 | 14 | public getAvatar(size: number): string { 15 | const twitterImgSize = this.getImageSize(size); 16 | return `https://twitter.com/${ 17 | this.sourceId 18 | }/profile_image?size=${twitterImgSize}`; 19 | } 20 | 21 | private getImageSize(size: number) { 22 | if (size <= 24) { 23 | return 'mini'; 24 | } 25 | 26 | if (size <= 48) { 27 | return 'normal'; 28 | } 29 | 30 | if (size <= 73) { 31 | return 'bigger'; 32 | } 33 | 34 | return 'original'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/value.ts: -------------------------------------------------------------------------------- 1 | import { Source } from './source'; 2 | import { AvatarSource } from './avatar-source.enum'; 3 | 4 | /** 5 | * Value source implementation. 6 | * return the value as avatar 7 | */ 8 | export class Value implements Source { 9 | readonly sourceType: AvatarSource = AvatarSource.VALUE; 10 | 11 | constructor(public sourceId: string) {} 12 | 13 | public getAvatar(): string { 14 | return this.sourceId; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/lib/sources/vkontakte.ts: -------------------------------------------------------------------------------- 1 | import { AsyncSource } from './async-source'; 2 | import { AvatarSource } from './avatar-source.enum'; 3 | 4 | /** 5 | * Vkontakte source implementation. 6 | * Fetch avatar source based on vkontakte identifier 7 | * and image size 8 | */ 9 | const apiVersion = 5.8; 10 | 11 | export class Vkontakte extends AsyncSource { 12 | readonly sourceType: AvatarSource = AvatarSource.VKONTAKTE; 13 | 14 | constructor(sourceId: string) { 15 | super(sourceId); 16 | } 17 | 18 | public getAvatar(size: number): string { 19 | const imgSize = this.getImageSize(size); 20 | return `https://api.vk.com/method/users.get?user_id=${ 21 | this.sourceId 22 | }&v=${apiVersion}&fields=${imgSize}`; 23 | } 24 | 25 | /** 26 | * extract vkontakte avatar from json data 27 | */ 28 | public processResponse(data: { 29 | response: { 30 | [key: string]: string; 31 | }[] 32 | }): string | null { 33 | // avatar key property is the size used to generate avatar url 34 | // size property is always the last key in the response object 35 | const sizeProperty = Object.keys(data['response'][0]).pop(); 36 | if (!sizeProperty) { 37 | return null; 38 | } 39 | // return avatar src 40 | return data['response'][0][sizeProperty] || null; 41 | } 42 | 43 | /** 44 | * Returns image size related to vkontakte API 45 | */ 46 | private getImageSize(size: number): string { 47 | if (size <= 50) { 48 | return 'photo_50'; 49 | } 50 | 51 | if (size <= 100) { 52 | return 'photo_100'; 53 | } 54 | 55 | if (size <= 200) { 56 | return 'photo_200'; 57 | } 58 | 59 | return 'photo_max'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /projects/ngx-avatar/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-avatar 3 | */ 4 | export * from './lib/avatar.module'; 5 | export * from './lib/avatar.component'; 6 | export * from './lib/avatar-config'; 7 | export * from './lib/avatar.service'; 8 | export * from './lib/sources/avatar-source.enum'; 9 | -------------------------------------------------------------------------------- /projects/ngx-avatar/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'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context( 13 | path: string, 14 | deep?: boolean, 15 | filter?: RegExp, 16 | ): { 17 | keys(): string[]; 18 | (id: string): T; 19 | }; 20 | }; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | -------------------------------------------------------------------------------- /projects/ngx-avatar/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "downlevelIteration": true, 15 | "types": [], 16 | "lib": [ 17 | "dom", 18 | "es2017" 19 | ] 20 | }, 21 | "angularCompilerOptions": { 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "flatModuleId": "AUTOGENERATED", 27 | "flatModuleOutFile": "AUTOGENERATED" 28 | }, 29 | "exclude": [ 30 | "src/test.ts", 31 | "**/*.spec.ts" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /projects/ngx-avatar/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "angularCompilerOptions": { 4 | "enableIvy": false 5 | } 6 | } -------------------------------------------------------------------------------- /projects/ngx-avatar/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 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/ngx-avatar/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "no-redundant-jsdoc": true, 5 | "directive-selector": [ 6 | true, 7 | "attribute", 8 | "lib", 9 | "camelCase" 10 | ], 11 | "component-selector": [ 12 | true, 13 | "element", 14 | "lib", 15 | "kebab-case" 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/copy-artifacts.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const artifacts = ["README.md", "LICENSE.txt"]; 5 | artifacts.forEach(artifact => { 6 | const fromPath = path.resolve(__dirname, "..", artifact); 7 | const toPath = path.resolve(__dirname, "..", "dist/ngx-avatar", artifact); 8 | 9 | fs.readFile(fromPath, "utf-8", (err, data) => { 10 | if (err) { 11 | console.log("an error occurred while reading file ", fromPath); 12 | return; 13 | } 14 | fs.writeFile(toPath, data, err => { 15 | if (err) { 16 | console.log("an error occurred while writing file ", toPath); 17 | return; 18 | } 19 | console.log(`${artifact} copied`); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Avatar from different sources

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |

Avatar with fallback

21 | 22 | 23 | 27 | 28 |

29 | 30 |

Initials & Value Avatar

31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |

40 | 41 | 42 | 43 | 44 | 45 |

Asynchrounous Avatars

46 | 47 | 48 | 49 |

Failed sources are not retried

50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 | 59 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | ngx-avatar { 2 | display:inline-block; 3 | } 4 | 5 | div { 6 | text-align: center; 7 | } -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 3 | import { TestBed, waitForAsync } from '@angular/core/testing'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { UserService } from './user.service'; 7 | 8 | const userServiceStub = { 9 | fetchInformation: jasmine.createSpy('userService.fetchInformation'), 10 | getUserFacebook: jasmine.createSpy('userService.getUserFacebook') 11 | }; 12 | 13 | describe('AppComponent', () => { 14 | beforeEach(waitForAsync(() => { 15 | TestBed.configureTestingModule({ 16 | declarations: [AppComponent], 17 | imports: [HttpClientTestingModule], 18 | providers: [{ provide: UserService, use: userServiceStub }], 19 | schemas: [NO_ERRORS_SCHEMA] 20 | }).compileComponents(); 21 | })); 22 | it('should create the app', waitForAsync(() => { 23 | const fixture = TestBed.createComponent(AppComponent); 24 | const app = fixture.debugElement.componentInstance; 25 | expect(app).toBeTruthy(); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { UserService } from './user.service'; 3 | import { Source } from '../../projects/ngx-avatar/src/lib/sources/source'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'] 9 | }) 10 | export class AppComponent implements OnInit { 11 | userName = 'Haithem Mosbahi'; 12 | userFB = 'wrongId'; 13 | customStyle = { 14 | backgroundColor: '#27ae60', 15 | border: '1px solid #bdc3c7', 16 | borderRadius: '50%', 17 | color: 'white', 18 | cursor: 'pointer' 19 | }; 20 | 21 | failedSources: number[] = []; 22 | 23 | constructor(public userService: UserService) {} 24 | 25 | ngOnInit() { 26 | this.userService.fetchInformation().subscribe(user => { 27 | this.userName = user.username; 28 | this.userService.getUserFacebook().subscribe(data => { 29 | this.userFB = data; 30 | }); 31 | }); 32 | } 33 | 34 | avatarClicked(event: Source) { 35 | alert('click on avatar fetched from ' + event.sourceType); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | 5 | import { AvatarModule } from 'ngx-avatar'; 6 | 7 | import { AppComponent } from './app.component'; 8 | import { UserService } from './user.service'; 9 | 10 | const avatarColors = ['#FFB6C1', '#2c3e50', '#95a5a6', '#f39c12', '#1abc9c']; 11 | 12 | @NgModule({ 13 | declarations: [AppComponent], 14 | imports: [ 15 | BrowserModule, 16 | HttpClientModule, 17 | AvatarModule.forRoot({ 18 | colors: avatarColors 19 | }) 20 | ], 21 | providers: [UserService], 22 | bootstrap: [AppComponent] 23 | }) 24 | export class AppModule {} 25 | -------------------------------------------------------------------------------- /src/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing", 3 | "private": true, 4 | "description_1": "This is a special package.json file that is not used by package managers.", 5 | "description_2": "It is used to tell the tools and bundlers whether the code under this directory is free of code with non-local side-effect. Any code that does have non-local side-effects can't be well optimized (tree-shaken) and will result in unnecessary increased payload size.", 6 | "description_3": "It should be safe to set this option to 'false' for new applications, but existing code bases could be broken when built with the production config if the application code does contain non-local side-effects that the application depends on.", 7 | "description_4": "To learn more about this file see: https://angular.io/config/app-package-json.", 8 | "sideEffects": false 9 | } 10 | -------------------------------------------------------------------------------- /src/app/user.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the User contract 3 | */ 4 | export interface User { 5 | username: string; 6 | facebookId: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { User } from './user.model'; 5 | import { HttpClient } from '@angular/common/http'; 6 | 7 | /** 8 | * Service used to fetch Async information about the user 9 | */ 10 | @Injectable() 11 | export class UserService { 12 | constructor(private http: HttpClient) { } 13 | 14 | fetchInformation(): Observable { 15 | return this.http.get('assets/data/data.json').pipe( 16 | map(response => response as User) 17 | ); 18 | } 19 | 20 | getUserFacebook(): Observable { 21 | return this.http.get<{ facebookId: string}>('assets/data/data.json').pipe( 22 | map(response => response.facebookId ) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HaithemMosbahi/ngx-avatar/22e9715e6c6499d22997982b2b46ca3323cbe683/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/data/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "username":"John Doe", 3 | "facebookId":"1508319875" 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HaithemMosbahi/ngx-avatar/22e9715e6c6499d22997982b2b46ca3323cbe683/src/assets/img/avatar.jpg -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HaithemMosbahi/ngx-avatar/22e9715e6c6499d22997982b2b46ca3323cbe683/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testing 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/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 | require('karma-mocha-reporter') 15 | ], 16 | client: { 17 | clearContext: false // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | coverageIstanbulReporter: { 20 | dir: require('path').join(__dirname, '../coverage'), 21 | reports: ['html', 'lcovonly'], 22 | fixWebpackSourcePaths: true 23 | }, 24 | reporters: ['coverage-istanbul', 'mocha'], 25 | port: 9876, 26 | colors: true, 27 | logLevel: config.LOG_INFO, 28 | autoWatch: true, 29 | browsers: ['Chrome'], 30 | singleRun: false 31 | }); 32 | }; -------------------------------------------------------------------------------- /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.log(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'; 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 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /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: { 11 | context( 12 | path: string, 13 | deep?: boolean, 14 | filter?: RegExp, 15 | ): { 16 | keys(): string[]; 17 | (id: string): T; 18 | }; 19 | }; 20 | 21 | // First, initialize the Angular testing environment. 22 | getTestBed().initTestEnvironment( 23 | BrowserDynamicTestingModule, 24 | platformBrowserDynamicTesting() 25 | ); 26 | // Then we find all the tests. 27 | const context = require.context('./', true, /\.spec\.ts$/); 28 | // And load the modules. 29 | context.keys().map(context); 30 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/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 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "importHelpers": true, 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "sourceMap": true, 12 | "declaration": false, 13 | "module": "es2020", 14 | "moduleResolution": "node", 15 | "emitDecoratorMetadata": true, 16 | "experimentalDecorators": true, 17 | "downlevelIteration": true, 18 | "target": "es2015", 19 | "strictNullChecks": true, 20 | "typeRoots": [ 21 | "node_modules/@types" 22 | ], 23 | "lib": [ 24 | "es2017", 25 | "dom" 26 | ], 27 | "paths": { 28 | "ngx-avatar": [ 29 | "dist/ngx-avatar" 30 | ], 31 | "ngx-avatar/*": [ 32 | "dist/ngx-avatar/*" 33 | ] 34 | } 35 | }, 36 | "angularCompilerOptions": { 37 | "strictInjectionParameters": true, 38 | "strictInputAccessModifiers": true, 39 | "strictTemplates": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-any": true, 49 | "no-console": [ 50 | true, 51 | "debug", 52 | "info", 53 | "time", 54 | "timeEnd", 55 | "trace" 56 | ], 57 | "no-construct": true, 58 | "no-debugger": true, 59 | "no-duplicate-super": true, 60 | "no-empty": false, 61 | "no-empty-interface": true, 62 | "no-eval": true, 63 | "no-inferrable-types": [ 64 | true, 65 | "ignore-params" 66 | ], 67 | "no-misused-new": true, 68 | "no-non-null-assertion": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "no-inputs-metadata-property": true, 121 | "no-outputs-metadata-property": true, 122 | "no-host-metadata-property": true, 123 | "no-input-rename": false, 124 | "no-output-rename": false, 125 | "use-lifecycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true, 129 | "no-redundant-jsdoc": true 130 | } 131 | } 132 | --------------------------------------------------------------------------------