├── .eslintrc.json
├── .github
└── workflows
│ ├── build-demo.yaml
│ ├── test-node.yaml
│ └── tests.yaml
├── .gitignore
├── .npmignore
├── .npmrc
├── LICENSE
├── README.md
├── babel.config.js
├── demo
├── demo.css
├── favicon.png
├── index.html
├── index.js
└── type-test.tsx
├── docs
└── example1.jpg
├── index.d.ts
├── package.json
├── src
├── avatar.js
├── cache.js
├── components
│ ├── image.js
│ ├── text.js
│ └── wrapper.js
├── context.js
├── data-provider.js
├── index.js
├── internal-state.js
├── sources
│ ├── AvatarRedirect.js
│ ├── Facebook.js
│ ├── Github.js
│ ├── Google.js
│ ├── Gravatar.js
│ ├── Icon.js
│ ├── Instagram.js
│ ├── Skype.js
│ ├── Src.js
│ ├── Twitter.js
│ ├── VKontakte.js
│ └── Value.js
└── utils.js
├── tsconfig.json
└── webpack.config.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "babel-eslint",
4 | "plugins": [ "react" ],
5 | "env": {
6 | "browser": true
7 | },
8 | "extends": "eslint:recommended",
9 | "rules": {
10 | "no-console": 2,
11 | "no-debugger": 0,
12 | "no-shadow": 2,
13 | "no-use-before-define": 2,
14 | "eqeqeq": 0,
15 | "curly": 0,
16 | "no-underscore-dangle": 0,
17 | "quotes": [2, "single"],
18 | "semi": [ 2, "always" ],
19 | "space-unary-ops": 0,
20 | "space-infix-ops": 2,
21 | "indent": [ 2, 4 ],
22 | "strict": 0,
23 | "jsx-quotes": 1,
24 | "no-multi-spaces": 2,
25 | "no-trailing-spaces": 2,
26 | "spaced-comment": [2, "always", { "block": { "exceptions": ["*"] } } ],
27 | "no-lonely-if": 2,
28 | "no-negated-condition": 2,
29 | "no-multiple-empty-lines": 2,
30 | "react/display-name": 1,
31 | "new-parens": 2,
32 | "new-cap": 2,
33 | "eol-last": 2,
34 | "no-const-assign": 2,
35 | "consistent-this": [2, "self"],
36 | "linebreak-style": [2, "unix"],
37 | "max-nested-callbacks": [2, 3],
38 | "no-class-assign": 2,
39 | "no-dupe-class-members": 2,
40 | "no-this-before-super": 2,
41 | "prefer-const": 1,
42 | "react/jsx-boolean-value": [2, "always"],
43 | "react/jsx-closing-bracket-location": [2, { "location": "after-props" }],
44 | "react/jsx-curly-spacing": 2,
45 | "react/jsx-indent-props": [2, 4],
46 | "react/jsx-max-props-per-line": [2, { "maximum": 4}],
47 | "react/jsx-no-duplicate-props": 2,
48 | "react/jsx-no-undef": 2,
49 | "react/jsx-uses-react": 2,
50 | "react/jsx-uses-vars": 2,
51 | "react/no-did-mount-set-state": 2,
52 | "react/no-did-update-set-state": 2,
53 | "react/no-unknown-property": 2,
54 | "react/prop-types": 2,
55 | "react/self-closing-comp": 2,
56 | "react/sort-comp": 2,
57 | "react/jsx-wrap-multilines": 2
58 | },
59 | "settings": {
60 | "react": {
61 | "version": "detect"
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/.github/workflows/build-demo.yaml:
--------------------------------------------------------------------------------
1 | name: 'Generate demo'
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | node:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v3
14 |
15 | - name: Setup node
16 | uses: actions/setup-node@v3
17 |
18 | - name: Install
19 | run: npm install
20 |
21 | - name: Run tests
22 | run: npm run build:demo
23 |
24 | - name: Publish to GitHub pages
25 | if: success()
26 | uses: crazy-max/ghaction-github-pages@v3
27 | with:
28 | target_branch: gh-pages
29 | build_dir: build
30 | env:
31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 |
--------------------------------------------------------------------------------
/.github/workflows/test-node.yaml:
--------------------------------------------------------------------------------
1 | name: Test a node version
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | node:
7 | required: true
8 | type: string
9 |
10 | jobs:
11 | node:
12 | concurrency: ${{ github.ref }}${{ inputs.node }}
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v3
17 |
18 | - name: Setup node
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: ${{ inputs.node }}
22 |
23 | - name: Install
24 | run: npm install
25 |
26 | - name: Run tests
27 | run: npm test
28 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: 'Run tests'
2 |
3 | on: pull_request
4 |
5 | permissions:
6 | contents: read
7 |
8 | jobs:
9 | node-latest:
10 | uses: ./.github/workflows/test-node.yaml
11 | with:
12 | node: latest
13 |
14 | node-18:
15 | uses: ./.github/workflows/test-node.yaml
16 | with:
17 | node: 18
18 |
19 | node-20:
20 | uses: ./.github/workflows/test-node.yaml
21 | with:
22 | node: 20
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build
3 | lib
4 | es
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | /build
3 | /demo
4 | /docs
5 | /src
6 | /*.config.js
7 | /.*
8 | .DS_Store
9 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Wim Mostmans
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # <Avatar> [](https://travis-ci.org/Sitebase/react-avatar) [](https://www.npmjs.com/package/react-avatar) [](https://www.npmjs.com/package/react-avatar)  
2 |
3 | Universal avatar makes it possible to fetch/generate an avatar based on the information you have about that user.
4 | We use a fallback system that if for example an invalid Facebook ID is used it will try Google, and so on.
5 |
6 | 
7 |
8 | For the moment we support following types:
9 |
10 | * Facebook
11 | * GitHub
12 | * Google (using [Avatar Redirect](#avatar-redirect))
13 | * Twitter (using [Avatar Redirect](#avatar-redirect))
14 | * Instagram (using [Avatar Redirect](#avatar-redirect))
15 | * Vkontakte (using [Avatar Redirect](#avatar-redirect))
16 | * Skype
17 | * Gravatar
18 | * Custom image
19 | * Name initials
20 |
21 | The fallbacks are in the same order as the list above were Facebook has the highest priority.
22 |
23 | ## Demo
24 |
25 | [Check it live!](https://ambassify.github.io/react-avatar/)
26 |
27 | ## Install
28 |
29 | Install the component using [NPM](https://www.npmjs.com/):
30 |
31 | ```sh
32 | $ npm install react-avatar --save
33 |
34 | # besides React, react-avatar also has prop-types as peer dependency,
35 | # make sure to install it into your project
36 | $ npm install prop-types --save
37 | ```
38 |
39 | Or [download as ZIP](https://github.com/sitebase/react-avatar/archive/master.zip).
40 |
41 | #### Note on usage in Gatsby projects
42 |
43 | Users of **Gatsby** who are experiencing issues with the latest release should install `react-avatar@corejs2` instead. This is an older version (v3.7.0) release of `react-avatar` that still used `core-js@2`.
44 |
45 | If you'd like to use the latest version of `react-avatar` have a look at [#187](https://github.com/Sitebase/react-avatar/issues/187) for a workaround and [#187](https://github.com/Sitebase/react-avatar/issues/187#issuecomment-587187113), [#181](https://github.com/Sitebase/react-avatar/issues/181) and [#198](https://github.com/Sitebase/react-avatar/issues/198) for a description of the issue.
46 |
47 |
48 | ## Usage
49 |
50 | 1. Import Custom Element:
51 |
52 | ```js
53 | import Avatar from 'react-avatar';
54 | ```
55 |
56 | 2. Start using it!
57 |
58 | ```html
59 |
60 | ```
61 |
62 | **Some examples:**
63 |
64 | ```html
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | ```
77 |
78 | **Manually generating a color:**
79 |
80 | ```html
81 | import Avatar from 'react-avatar';
82 |
83 |
84 | ```
85 |
86 | **Configuring React Avatar globally**
87 |
88 | ```html
89 | import Avatar, { ConfigProvider } from 'react-avatar';
90 |
91 |
92 |
93 | ...
94 |
95 | ...
96 |
97 |
98 |
99 | ```
100 |
101 | ## Options
102 |
103 | ### Avatar
104 |
105 | | Attribute | Options | Default | Description |
106 | | ------------- | ----------------- | ------- | ------------------------------------------------------------------------------------------------------ |
107 | | `className` | *string* | | Name of the CSS class you want to add to this component alongside the default `sb-avatar`. |
108 | | `email` | *string* | | String of the email address of the user. |
109 | | `md5Email` | *string* | | String of the MD5 hash of email address of the user. |
110 | | `facebookId` | *string* | | |
111 | | `twitterHandle` | *string* | | |
112 | | `instagramId` | *string* | | |
113 | | `googleId` | *string* | | |
114 | | `githubHandle`| *string* | | String of the user's GitHub handle. |
115 | | `skypeId` | *string* | | |
116 | | `name` | *string* | | Will be used to generate avatar based on the initials of the person |
117 | | `maxInitials` | *number* | | Set max nr of characters used for the initials. If maxInitials=2 and the name is Foo Bar Var the initials will be FB |
118 | | `initials` | *string or function* | [defaultInitials][3] | Set the initials to show or a function that derives them from the component props, the method should have the signature `fn(name, props)` |
119 | | `value` | *string* | | Show a value as avatar |
120 | | `alt` | *string* | `name` or `value` | The `alt` attribute used on the avatar `img` tag. If not set we will fallback to either `name` or `value` |
121 | | `title` | *string* | `name` or `value` | The `title` attribute used on the avatar `img` tag. If not set we will fallback to either `name` or `value` |
122 | | `color` | *string* | random | Used in combination with `name` and `value`. Give the background a fixed color with a hex like for example #FF0000 |
123 | | `fgColor` | *string* | #FFF | Used in combination with `name` and `value`. Give the text a fixed color with a hex like for example #FF0000 |
124 | | `size` | *[length][1]* | 50px | Size of the avatar |
125 | | `textSizeRatio` | *number* | 3 | For text based avatars the size of the text as a fragment of size (size / textSizeRatio) |
126 | | `textMarginRatio` | *number* | .15 | For text based avatars. The size of the minimum margin between the text and the avatar's edge, used to make sure the text will always fit inside the avatar. (calculated as `size * textMarginRatio`) |
127 | | `round` | *bool or [length][1]* | false | The amount of `border-radius` to apply to the avatar corners, `true` shows the avatar in a circle. |
128 | | `src` | *string* | | Fallback image to use |
129 | | `style` | *object* | | Style that will be applied on the root element |
130 | | `unstyled` | *bool* | false | Disable all styles |
131 | | `onClick` | *function* | | Mouse click event |
132 |
133 | ### ConfigProvider
134 |
135 | | Attribute | Options | Default | Description |
136 | | ------------- | ----------------- | ------- | ------------------------------------------------------------------------------------------------------ |
137 | | `colors` | *array(string)* | [default colors](https://github.com/Sitebase/react-avatar/tree/master/src/utils.js#L39-L47) | A list of color values as strings from which the `getRandomColor` picks one at random. |
138 | | `cache` | *[cache](#implementing-a-custom-cache)* | [internal cache](https://github.com/Sitebase/react-avatar/tree/master/src/cache.js) | Cache implementation used to track broken img URLs |
139 | | `initials` | *function* | [defaultInitials][3] | A function that derives the initials from the component props, the method should have the signature `fn(name, props)` |
140 | | `avatarRedirectUrl` | *URL* | `undefined` | Base URL to a [Avatar Redirect](#avatar-redirect) instance |
141 |
142 | **Example**
143 |
144 | ```html
145 | import Avatar, { ConfigProvider } from 'react-avatar';
146 |
147 |
148 |
149 | ...
150 |
151 | ...
152 |
153 |
154 |
155 | ```
156 |
157 | ### Cache
158 |
159 | This class represents the default implementation of the cache used by react-avatar.
160 |
161 | Looking to implement more complex [custom cache behaviour](#implementing-a-custom-cache)?
162 |
163 | | Attribute | Options | Default | Description |
164 | | ------------- | ----------------- | ------- | ------------------------------------------------------------------------------------------------------ |
165 | | `cachePrefix` | *string* | `react-avatar/` | The prefix for `localStorage` keys used by the cache. |
166 | | `sourceTTL` | *number* | 604800000 (7 days) | The amount of time a failed source is kept in cache. (in milliseconds) |
167 | | `sourceSize` | *number* | 20 | The maximum number of failed source entries to keep in cache at any time. |
168 |
169 | **usage**
170 |
171 | ```html
172 | import Avatar, { Cache, ConfigProvider } from 'react-avatar';
173 |
174 | const cache = new Cache({
175 |
176 | // Keep cached source failures for up to 7 days
177 | sourceTTL: 7 * 24 * 3600 * 1000,
178 |
179 | // Keep a maximum of 20 entries in the source cache
180 | sourceSize: 20
181 | });
182 |
183 | // Apply cache globally
184 |
185 |
186 | ...
187 |
188 | ...
189 |
190 |
191 |
192 | // For specific instances
193 |
194 |
195 | ```
196 |
197 | ### Avatar Redirect
198 |
199 | [Avatar Redirect][2] adds support for social networks which require a server-side service to find the correct avatar URL.
200 |
201 | Examples of this are:
202 |
203 | - Twitter
204 | - Instagram
205 |
206 | An open Avatar Redirect endpoint is provided at `https://avatar-redirect.appspot.com`. However this endpoint is provided for free and as such an explicit opt-in is required as no guarantees can be made about uptime of this endpoint.
207 |
208 | Avatar Redirect is enabled by setting the `avatarRedirectUrl` property on the [ConfigProvider context](#configprovider)
209 |
210 | ## Development
211 |
212 | In order to run it locally you'll need to fetch some dependencies and a basic server setup.
213 |
214 | * Install local dependencies:
215 |
216 | ```sh
217 | $ npm install
218 | ```
219 |
220 | * To test your react-avatar and your changes, start the development server and open `http://localhost:8000/index.html`.
221 |
222 | ```sh
223 | $ npm run dev
224 | ```
225 |
226 | * To create a local production build into the `lib` and `es` folders.
227 |
228 | ```sh
229 | $ npm run build
230 | ```
231 |
232 | ### Implementing a custom cache
233 |
234 | `cache` as provided to the `ConfigProvider` should be an object implementing the methods below. The default cache implementation can be found [here](https://github.com/Sitebase/react-avatar/tree/master/src/cache.js)
235 |
236 | | Method | Description |
237 | | ------------- | ------------------------------------------------------------------------------------------------------ |
238 | | `set(key, value)` | Save `value` at `key`, such that it can be retrieved using `get(key)`. Returns `undefined` |
239 | | `get(key)` | Retrieve the value stored at `key`, if the cache does not contain a value for `key` return `null` |
240 | | `sourceFailed(source)` | Mark the image URL specified in `source` as failed. Returns `undefined` |
241 | | `hasSourceFailedBefore(source)` | Returns `true` if the `source` has been tagged as failed using `sourceFailed(source)`, otherwise `false`. |
242 |
243 | ## Reducing bundle size
244 |
245 | ### Webpack 4
246 |
247 | When using webpack 4 you can rely on [tree shaking](https://webpack.js.org/guides/tree-shaking/) to drop unused sources when creating your Avatar component like the example below.
248 |
249 | ```javascript
250 | import { createAvatarComponent, TwitterSource } from 'react-avatar';
251 |
252 | const Avatar = createAvatarComponent({
253 | sources: [ TwitterSource ]
254 | });
255 | ```
256 |
257 | Exported sources:
258 | - GravatarSource
259 | - FacebookSource
260 | - GithubSource
261 | - SkypeSource
262 | - ValueSource
263 | - SrcSource
264 | - IconSource
265 | - VKontakteSource
266 | - InstagramSource
267 | - TwitterSource
268 | - GoogleSource
269 | - RedirectSource
270 |
271 | ### Without Webpack >= 4
272 |
273 | If you are using a version of webpack that does not support tree shaking or are using a different bundler you'll need to import only those files you need.
274 |
275 | #### ES6 modules
276 | ```javascript
277 | import createAvatarComponent from 'react-avatar/es/avatar';
278 | import TwitterSource from 'react-avatar/es/sources/Twitter';
279 |
280 | const Avatar = createAvatarComponent({
281 | sources: [ TwitterSource ]
282 | });
283 | ```
284 |
285 | #### Transpiled ES5 javascript / commonjs
286 | ```javascript
287 | const createAvatarComponent = require('react-avatar/lib/avatar').default;
288 | const TwitterSource = require('react-avatar/lib/sources/Twitter').default;
289 |
290 | const Avatar = createAvatarComponent({
291 | sources: [ TwitterSource ]
292 | });
293 | ```
294 |
295 | ## Products using React Avatar
296 |
297 | * [Ambassify](https://www.ambassify.com/?utm_source=github&utm_medium=readme&utm_campaign=react-avatar)
298 |
299 | ## Contributing
300 |
301 | 1. Fork it!
302 | 2. Create your feature branch: `git checkout -b my-new-feature`
303 | 3. Commit your changes: `git commit -m 'Add some feature'`
304 | 4. Push to the branch: `git push origin my-new-feature`
305 | 5. Submit a pull request :D
306 |
307 | ## History
308 |
309 | For detailed changelog, check [Releases](https://github.com/sitebase/react-avatar/releases).
310 |
311 | ## Maintainers
312 |
313 | - [@Sitebase](https://github.com/Sitebase) (Creator)
314 | - [@JorgenEvens](https://github.com/JorgenEvens)
315 |
316 | ## License
317 |
318 | [MIT License](http://opensource.org/licenses/MIT)
319 |
320 | [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/length
321 | [2]: https://github.com/JorgenEvens/avatar-redirect
322 | [3]: https://github.com/Sitebase/react-avatar/blob/master/src/utils.js
323 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | const { BABEL_MODULES = false } = process.env;
4 |
5 | module.exports = {
6 | targets: '> 0.25%, not dead',
7 | presets: [
8 | [ '@babel/preset-env', { modules: BABEL_MODULES } ],
9 | '@babel/preset-react'
10 | ],
11 | plugins: [
12 | '@babel/plugin-proposal-class-properties',
13 | '@babel/plugin-transform-runtime',
14 | [ 'polyfill-corejs3', { method: 'usage-pure' } ],
15 | ]
16 | };
17 |
--------------------------------------------------------------------------------
/demo/demo.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
5 | body {
6 | line-height: 1.7em;
7 | color: #7f8c8d;
8 | font-size: 14px;
9 | font-family: 'Roboto Slab', Arial, sans-serif;
10 | text-align:center;
11 | }
12 | a:link, a:active, a:visited {
13 | color: #fff;
14 | text-decoration: underline;
15 | }
16 | a:hover {
17 | text-decoration: none;
18 | }
19 | p {
20 | padding: 5px 0;
21 | }
22 |
23 | h1 {
24 | font-size: 70px;
25 | font-weight: 100;
26 | line-height: 1em;
27 | margin-bottom: 30px;
28 | }
29 | h2 {
30 | padding: 10px 0 20px 0;
31 | }
32 | #header {
33 | background: #7e3794;
34 | padding: 50px 20px;
35 | color: #FFF;
36 | margin-bottom: 30px;
37 | }
38 | #container {
39 | }
40 | section {
41 | margin: 0 0 30px 0;
42 | }
43 | .sb-avatar {
44 | margin: 5px;
45 | }
46 | button {
47 | border: none;
48 | background: #7e3794;
49 | margin: 5px;
50 | color: #fff;
51 | text-transform: uppercase;
52 | font-size: 14px;
53 | padding: 15px;
54 | border-radius: 3px;
55 | }
56 | .hidden {
57 | display: none;
58 | }
59 |
--------------------------------------------------------------------------------
/demo/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambassify/react-avatar/1a22019f32f7f636ed52f982363618d7dae79b69/demo/favicon.png
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React Avatar Component
6 |
7 |
8 |
9 |
11 |
12 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import Avatar, {
6 | getRandomColor,
7 | ConfigProvider,
8 | createAvatarComponent,
9 | GravatarSource,
10 | ValueSource
11 | } from './../src';
12 |
13 | const CustomAvatar = createAvatarComponent({
14 | sources: [ GravatarSource, ValueSource ]
15 | });
16 |
17 | import './index.html';
18 | import './demo.css';
19 | import './favicon.png';
20 |
21 | const customColors = [
22 | '#5E005E',
23 | '#AB2F52',
24 | '#E55D4A',
25 | '#E88554',
26 | '#4194A6',
27 | '#82CCD9',
28 | '#FFCC6B',
29 | '#F2855C',
30 | '#7D323B'
31 | ];
32 |
33 | export default
34 | class Demo extends React.Component {
35 | static displayName = 'Demo';
36 |
37 | state = {
38 | name: 'Wim Mostmans',
39 | skypeId: null,
40 | toggle: true,
41 | color: customColors[0]
42 | }
43 |
44 | _onToggleName = () => {
45 | this.setState({
46 | name: 'Foo Bar',
47 | skypeId: null
48 | });
49 | }
50 |
51 | _onChangeName = (e) => {
52 | this.setState({
53 | name: e.target.value
54 | });
55 | }
56 |
57 | _onSetSkype = () => {
58 | this.setState({skypeId: 'sitebase'});
59 | }
60 |
61 | _onClick = () => {
62 | alert('Clicked!');
63 | }
64 |
65 | _onToggle = () => {
66 | this.setState({
67 | toggle: !this.state.toggle
68 | });
69 | }
70 |
71 | _onToggleColor = () => {
72 | const current = customColors.indexOf(this.state.color);
73 | const next = (current + 1) % customColors.length;
74 |
75 | this.setState({ color: customColors[next] });
76 | }
77 |
78 | _onAttachRef(ref) {
79 | // Dummy function to test errors on reference
80 | if(console) {
81 | // eslint-disable-next-line no-console
82 | console.log('Ref received', ref);
83 | }
84 | }
85 |
86 | render() {
87 | return (
88 |
89 |
99 |
100 | Gravatar
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | Invalid gravatar
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | Initials Text Size
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | Google
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | Facebook
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | Twitter using Avatar Redirect
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 | Instagram using Avatar Redirect
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | Vkontakte
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | Skype
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | Github
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | Initials
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 | onClick
201 |
202 |
203 |
204 |
205 | Initials with different font sizes
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 | Size in different units
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
238 |
241 |
244 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 | Initials are always restrained to a margin
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
281 |
284 |
287 |
290 |
291 |
292 |
293 |
294 | Custom colors
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 | Nullable title
305 |
306 |
310 |
315 |
320 |
325 |
326 |
327 |
328 |
329 | Initials with maximum number of characters
330 |
331 |
333 |
335 |
336 |
337 |
338 |
339 |
340 |
341 | Value
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 | Fallback to static src
350 |
351 |
352 |
353 |
354 | Double fallback: Facebook to Google to initials
355 |
360 |
361 |
362 | Custom style
363 |
367 |
368 |
369 | Unstyled
370 |
373 |
374 |
375 | Vertical Alignment
376 |
377 | Wim Mostmans
378 |
379 | Wim Mostmans
380 |
381 | Wim Mostmans
382 |
383 | Wim Mostmans
384 |
385 |
386 | Toggle with cached Avatars
387 |
388 |
389 |
390 | {this.state.toggle &&
391 |
392 |
393 |
394 |
395 |
396 |
397 | }
398 |
399 |
400 | Toggle color
401 |
402 |
403 |
404 |
405 | {this.state.color}
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 | Configuration Context
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
name.split(/\s+/)[0]}>
428 |
429 | Custom Initials Function
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 | Avatar with only support for gravatar and value
441 |
442 |
443 |
444 |
445 |
446 |
447 | );
448 | }
449 | }
450 |
451 | var mountNode = document.getElementById('container');
452 |
453 | // Enable strict mode when supported by react version
454 | var Wrapper = React.StrictMode || 'div';
455 |
456 | ReactDOM.render((
457 |
458 |
459 |
460 | ), mountNode);
461 |
--------------------------------------------------------------------------------
/demo/type-test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains a very small demo project
3 | * that is tested for type correctness in the
4 | * `npm run test:type-check` check.
5 | *
6 | * This file is intended to include any API interface
7 | * to ensure that our type definitions are correct.
8 | */
9 |
10 | import React, { Component } from 'react';
11 | import Avatar, {
12 | createAvatarComponent,
13 | ConfigProvider,
14 | Cache,
15 |
16 | GravatarSource,
17 | FacebookSource,
18 | GithubSource,
19 | SkypeSource,
20 | ValueSource,
21 | SrcSource,
22 | IconSource,
23 |
24 | VKontakteSource,
25 | InstagramSource,
26 | TwitterSource,
27 | GoogleSource,
28 | } from '..';
29 |
30 | const CustomAvatar = createAvatarComponent({
31 | sources: [
32 | GravatarSource,
33 | FacebookSource,
34 | GithubSource,
35 | SkypeSource,
36 | ValueSource,
37 | SrcSource,
38 | IconSource,
39 | VKontakteSource,
40 | InstagramSource,
41 | TwitterSource,
42 | GoogleSource,
43 | ]
44 | });
45 |
46 | export default
47 | class TypeTest extends Component {
48 |
49 | render() {
50 | return (
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | )
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/docs/example1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambassify/react-avatar/1a22019f32f7f636ed52f982363618d7dae79b69/docs/example1.jpg
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export interface ReactAvatarProps {
4 | children?: React.ReactNode;
5 | /**
6 | * Name of the CSS class you want to add to this component alongside the default sb-avatar.
7 | */
8 | className?: string;
9 | /**
10 | * String of the email address of the user.
11 | */
12 | email?: string;
13 | /**
14 | * String of the MD5 hash of email address of the user.
15 | */
16 | md5Email?: string;
17 | facebookId?: string;
18 | twitterHandle?: string;
19 | googleId?: string;
20 | instagramId?: string;
21 | githubHandle?: string;
22 | skypeId?: string;
23 | /**
24 | * Will be used to generate avatar based on the initials of the person
25 | */
26 | name?: string;
27 | /**
28 | * Set max nr of characters used for the initials. If maxInitials=2 and the name is Foo Bar Var the initials will be FB
29 | */
30 | maxInitials?: number;
31 | /**
32 | * Initials to show or a method converting name into initials
33 | * @param {string} name
34 | * @param {any} value
35 | * @returns {string}
36 | */
37 | initials?: string | ((name: string, props: any) => string);
38 | /**
39 | * Show a value as avatar
40 | */
41 | value?: string;
42 | /**
43 | * The alt attribute used on the avatar img tag. If not set we will fallback to either name or value
44 | */
45 | alt?: string | boolean;
46 | /**
47 | * The title attribute used on the avatar img tag. If not set we will fallback to either name or value
48 | */
49 | title?: string | boolean;
50 | /**
51 | * Used in combination with `name` and `value`. Give the background a fixed color with a hex like for example #FF0000
52 | */
53 | color?: string;
54 | /**
55 | * Used in combination with `name` and `value`. Give the text a fixed color with a hex like for example #FF0000
56 | */
57 | fgColor?: string;
58 | /**
59 | * Size of the avatar
60 | */
61 | size?: string;
62 | /**
63 | * For text based avatars the size of the text as a fragment of size (size / textSizeRatio)
64 | */
65 | textSizeRatio?: number;
66 | /**
67 | * For text based avatars. The size of the minimum margin between the text and the avatar's edge, used to make sure the text will always fit inside the avatar. (calculated as `size * textMarginRatio`)
68 | */
69 | textMarginRatio?: number;
70 | /**
71 | * The amount of `border-radius` to apply to the avatar corners, `true` shows the avatar in a circle.
72 | */
73 | round?: boolean | string;
74 | /**
75 | * Fallback image to use
76 | */
77 | src?: string;
78 | /**
79 | * Style that will be applied on the root element
80 | */
81 | style?: any;
82 | /**
83 | * Disable all styles
84 | */
85 | unstyled?: boolean;
86 | /**
87 | * Mouse click event
88 | * @param {React.SyntheticEvent} e
89 | * @returns {any}
90 | */
91 | onClick?: (e: React.SyntheticEvent) => any;
92 | }
93 |
94 | interface CreateAvatarOptions {
95 | sources?: SourceConstructor[]
96 | }
97 |
98 | export interface ConfigProvider {
99 | /**
100 | * A list of color values as strings from which the getRandomColor picks one at random.
101 | */
102 | colors?: string[];
103 | /**
104 | * Cache implementation used to track broken img URLs
105 | */
106 | cache?: Cache;
107 | /**
108 | * Method converting name into initials
109 | * @param {string} name
110 | * @param {any} value
111 | * @returns {string}
112 | */
113 | initials?: (name: string, props: any) => string;
114 | /**
115 | * The baseUrl for a avatar-redirect service
116 | */
117 | avatarRedirectUrl?: string;
118 | }
119 |
120 | export interface Cache {
121 | /**
122 | * Save `value` at `key`, such that it can be retrieved using `get(key)`
123 | * @param {string} key
124 | * @param {string} value
125 | */
126 | set: (key: string, value: string) => void;
127 | /**
128 | * Retrieve the value stored at `key`, if the cache does not contain a value for `key` return `null`
129 | * @param {string} key
130 | * @returns {string | null}
131 | */
132 | get: (key: string) => string | null;
133 | /**
134 | * Mark the image URL specified in `source` as failed.
135 | * @param {string} source
136 | */
137 | sourceFailed: (source: string) => void;
138 | /**
139 | * Returns `true` if the `source` has been tagged as failed using `sourceFailed(source)`, otherwise `false`.
140 | * @param {string} source
141 | * @returns {boolean}
142 | */
143 | hasSourceFailedBefore: (source: string) => boolean;
144 | }
145 |
146 | export interface CacheOptions {
147 | cachePrefix?: string,
148 | sourceTTL?: number,
149 | sourceSize?: number
150 | }
151 |
152 | type CacheConstructor = new (options: CacheOptions) => Cache;
153 |
154 | interface Source {
155 | isCompatible: () => boolean;
156 | get: (setState: (update: object) => void) => void;
157 | }
158 |
159 | type SourceConstructor = new (props: object) => Source;
160 |
161 | export const RedirectSource: (network: string, property: string) => SourceConstructor
162 |
163 | /**
164 | * Universal avatar makes it possible to fetch/generate an avatar based on the information you have about that user.
165 | * We use a fallback system that if for example an invalid Facebook ID is used it will try Google, and so on.
166 | */
167 | type ReactAvatar = React.ComponentType;
168 | declare const Avatar : ReactAvatar;
169 |
170 | export const createAvatarComponent: (options: CreateAvatarOptions) => ReactAvatar;
171 |
172 | export const ConfigProvider: React.ComponentType;
173 | export const Cache: CacheConstructor;
174 |
175 | export const GravatarSource: SourceConstructor;
176 | export const FacebookSource: SourceConstructor;
177 | export const GithubSource: SourceConstructor;
178 | export const SkypeSource: SourceConstructor;
179 | export const ValueSource: SourceConstructor;
180 | export const SrcSource: SourceConstructor;
181 | export const IconSource: SourceConstructor;
182 |
183 | export const VKontakteSource: SourceConstructor;
184 | export const InstagramSource: SourceConstructor;
185 | export const TwitterSource: SourceConstructor;
186 | export const GoogleSource: SourceConstructor;
187 |
188 | export default Avatar;
189 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-avatar",
3 | "version": "5.0.3",
4 | "description": "Universal React avatar component makes it possible to generate avatars based on user information.",
5 | "main": "lib/index.js",
6 | "module": "es/index.js",
7 | "types": "index.d.ts",
8 | "typings": "index.d.ts",
9 | "sideEffects": false,
10 | "scripts": {
11 | "prepack": "npm -s run test && npm -s run build",
12 | "clean": "rm -rf ./{lib,es}/*",
13 | "build": "npm -s run clean && npm -s run build:commonjs && npm -s run build:modules",
14 | "build:commonjs": "BABEL_MODULES=commonjs babel ./src --out-dir ./lib",
15 | "build:modules": "babel ./src --out-dir ./es",
16 | "build:demo": "NODE_ENV=production webpack",
17 | "demo": "npm -s run build:demo",
18 | "dev": "webpack-dev-server",
19 | "serve": "npm -s run dev",
20 | "test": "npm -s run test:lint && npm -s run test:type-check",
21 | "test:lint": "eslint src",
22 | "test:type-check": "tsc -p tsconfig.json",
23 | "postpublish": "npm -s run publish:docs",
24 | "publish:docs": "npm -s run build:demo && gh-pages -d build"
25 | },
26 | "license": "MIT",
27 | "repository": {
28 | "type": "git",
29 | "url": "https://github.com/ambassify/react-avatar.git"
30 | },
31 | "author": "Wim Mostmans ",
32 | "bugs": {
33 | "url": "https://github.com/ambassify/react-avatar/issues"
34 | },
35 | "keywords": [
36 | "component",
37 | "reactjs",
38 | "react-component",
39 | "avatar"
40 | ],
41 | "homepage": "https://ambassify.github.io/react-avatar/",
42 | "peerDependencies": {
43 | "@babel/runtime": ">=7",
44 | "core-js-pure": ">=3",
45 | "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
46 | "prop-types": "^15.0.0 || ^16.0.0"
47 | },
48 | "devDependencies": {
49 | "@babel/cli": "^7.7.7",
50 | "@babel/core": "^7.7.7",
51 | "@babel/plugin-proposal-class-properties": "^7.7.4",
52 | "@babel/plugin-transform-runtime": "^7.16.0",
53 | "@babel/preset-env": "^7.7.7",
54 | "@babel/preset-react": "^7.7.4",
55 | "@babel/runtime": "^7.16.0",
56 | "@types/react": "^16.0.0",
57 | "babel-eslint": "^10.0.3",
58 | "babel-loader": "^8.0.6",
59 | "babel-plugin-polyfill-corejs3": "^0.3.0",
60 | "core-js-pure": "^3.19.1",
61 | "eslint": "^6.8.0",
62 | "eslint-loader": "^3.0.3",
63 | "eslint-plugin-react": "^7.17.0",
64 | "file-loader": "^5.0.2",
65 | "gh-pages": "^6.1.1",
66 | "react": "^17.0.1",
67 | "react-dom": "^17.0.1",
68 | "typescript": "^5.4.5",
69 | "webpack": "^5.72.1",
70 | "webpack-bundle-analyzer": "^3.6.0",
71 | "webpack-cli": "^4.9.2",
72 | "webpack-dev-server": "^4.9.1"
73 | },
74 | "dependencies": {
75 | "is-retina": "^1.0.3",
76 | "md5": "^2.0.0"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/avatar.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 |
5 | import { Cache } from './cache';
6 | import {withConfig, ConfigProvider} from './context';
7 | import createAvatarDataProvider from './data-provider';
8 | import {getRandomColor} from './utils';
9 |
10 | import Image from './components/image';
11 | import Text from './components/text';
12 |
13 | export {getRandomColor} from './utils';
14 | export {ConfigProvider} from './context';
15 | export {Cache} from './cache';
16 |
17 | export default
18 | function createAvatarComponent(options) {
19 |
20 | const DataProvider = createAvatarDataProvider(options);
21 |
22 | const Component = withConfig(
23 | // eslint-disable-next-line react/display-name
24 | React.forwardRef((props, ref) => (
25 |
26 | {avatar => {
27 | const Avatar = avatar.src ? Image : Text;
28 |
29 | return (
30 |
33 | );
34 | }}
35 |
36 | ))
37 | );
38 |
39 | return Object.assign(Component, {
40 | getRandomColor,
41 | ConfigProvider,
42 | Cache
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/src/cache.js:
--------------------------------------------------------------------------------
1 | export const CACHE_PREFIX = 'react-avatar/';
2 | export const CACHE_KEY_FAILING = 'failing';
3 |
4 | const _hasLocalStorage = (function isLocalStorageAvailable() {
5 | try {
6 | return ('localStorage' in window && window['localStorage']);
7 | } catch(err) {
8 | return false;
9 | }
10 | }());
11 |
12 | export
13 | class Cache {
14 |
15 | constructor(options = {}) {
16 | const {
17 | cachePrefix = CACHE_PREFIX,
18 | sourceTTL = 7 * 24 * 3600 * 1000,
19 | sourceSize = 20
20 | } = options;
21 |
22 | this.cachePrefix = cachePrefix;
23 | this.sourceTTL = sourceTTL;
24 | this.sourceSize = sourceSize;
25 | }
26 |
27 | set(key, value) {
28 | // cache not available
29 | if (!_hasLocalStorage)
30 | return;
31 |
32 | value = JSON.stringify(value);
33 |
34 | try {
35 | localStorage.setItem(this.cachePrefix + key, value);
36 | } catch(e) {
37 | // failsafe for mobile Safari private mode
38 | console.error(e); // eslint-disable-line no-console
39 | }
40 | }
41 |
42 | get(key) {
43 | // cache not available
44 | if (!_hasLocalStorage)
45 | return null;
46 |
47 | const value = localStorage.getItem(this.cachePrefix + key);
48 |
49 | if (value)
50 | return JSON.parse(value);
51 |
52 | return null;
53 | }
54 |
55 | sourceFailed(source) {
56 | let cacheList = this.get(CACHE_KEY_FAILING) || [];
57 |
58 | // Remove expired entries or previous instances of this source
59 | cacheList = cacheList.filter(entry => {
60 | const hasExpired = entry.expires > 0 && entry.expires < Date.now();
61 | const isMatch = entry === source || entry.url == source;
62 |
63 | return !hasExpired && !isMatch;
64 | });
65 |
66 | // Add the source to the end of the list
67 | cacheList.unshift({
68 | url: source,
69 | expires: Date.now() + this.sourceTTL
70 | });
71 |
72 | // only keep the last X results so we don't fill up local storage
73 | cacheList = cacheList.slice(0, this.sourceSize - 1);
74 |
75 | return this.set(CACHE_KEY_FAILING, cacheList);
76 | }
77 |
78 | hasSourceFailedBefore(source) {
79 | const cacheList = this.get(CACHE_KEY_FAILING) || [];
80 |
81 | return cacheList.some(entry => {
82 | const hasExpired = entry.expires > 0 && entry.expires < Date.now();
83 | const isMatch = entry === source || entry.url == source;
84 |
85 | return isMatch && !hasExpired;
86 | });
87 | }
88 | }
89 |
90 | export default new Cache();
91 |
--------------------------------------------------------------------------------
/src/components/image.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { parseSize, calculateBorderRadius, getNullableText } from '../utils';
5 | import Wrapper from './wrapper';
6 |
7 | export default
8 | class AvatarImage extends React.PureComponent {
9 |
10 | static propTypes = {
11 | alt: PropTypes.oneOfType([
12 | PropTypes.string,
13 | PropTypes.bool
14 | ]),
15 | title: PropTypes.oneOfType([
16 | PropTypes.string,
17 | PropTypes.bool
18 | ]),
19 | name: PropTypes.string,
20 | value: PropTypes.string,
21 | avatar: PropTypes.object,
22 |
23 | className: PropTypes.string,
24 | unstyled: PropTypes.bool,
25 | round: PropTypes.oneOfType([
26 | PropTypes.bool,
27 | PropTypes.string,
28 | PropTypes.number,
29 | ]),
30 | size: PropTypes.oneOfType([
31 | PropTypes.number,
32 | PropTypes.string
33 | ]),
34 | }
35 |
36 | static defaultProps = {
37 | className: '',
38 | round: false,
39 | size: 100,
40 | unstyled: false
41 | }
42 |
43 | render() {
44 | const {
45 | className,
46 | round,
47 | unstyled,
48 | alt,
49 | title,
50 | name,
51 | value,
52 | avatar
53 | } = this.props;
54 |
55 | const size = parseSize(this.props.size);
56 |
57 | const imageStyle = unstyled ? null : {
58 | maxWidth: '100%',
59 | width: size.str,
60 | height: size.str,
61 | borderRadius: calculateBorderRadius(round),
62 | };
63 |
64 | return (
65 |
66 |
74 |
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/text.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import Wrapper from './wrapper';
5 | import {
6 | parseSize,
7 | setGroupedTimeout,
8 | calculateBorderRadius,
9 | getNullableText
10 | } from '../utils';
11 |
12 | export default
13 | class AvatarText extends React.PureComponent {
14 |
15 | static propTypes = {
16 | name: PropTypes.string,
17 | value: PropTypes.string,
18 | avatar: PropTypes.object,
19 |
20 | title: PropTypes.oneOfType([
21 | PropTypes.string,
22 | PropTypes.bool
23 | ]),
24 |
25 | className: PropTypes.string,
26 | unstyled: PropTypes.bool,
27 | fgColor: PropTypes.string,
28 | textSizeRatio: PropTypes.number,
29 | textMarginRatio: PropTypes.number,
30 | round: PropTypes.oneOfType([
31 | PropTypes.bool,
32 | PropTypes.string,
33 | PropTypes.number,
34 | ]),
35 | size: PropTypes.oneOfType([
36 | PropTypes.number,
37 | PropTypes.string
38 | ]),
39 | }
40 |
41 | static defaultProps = {
42 | className: '',
43 | fgColor: '#FFF',
44 | round: false,
45 | size: 100,
46 | textSizeRatio: 3,
47 | textMarginRatio: .15,
48 | unstyled: false
49 | }
50 |
51 | componentDidMount() {
52 | this._mounted = true;
53 | this._scaleTextNode(this._node);
54 | }
55 |
56 | componentWillUnmount() {
57 | this._mounted = false;
58 | }
59 |
60 | _scaleTextNode = (node, retryTTL = 16) => {
61 | const {
62 | unstyled,
63 | textSizeRatio,
64 | textMarginRatio,
65 | avatar
66 | } = this.props;
67 |
68 | this._node = node;
69 |
70 | if (!node || !node.parentNode || unstyled || avatar.src || !this._mounted)
71 | return;
72 |
73 | const spanNode = node.parentNode;
74 | const tableNode = spanNode.parentNode;
75 |
76 | const {
77 | width: containerWidth,
78 | height: containerHeight
79 | } = spanNode.getBoundingClientRect();
80 |
81 | // Whenever the avatar element is not visible due to some CSS
82 | // (such as display: none) on any parent component we will check
83 | // whether the component has become visible.
84 | //
85 | // The time between checks grows up to half a second in an attempt
86 | // to reduce flicker / performance issues.
87 | if (containerWidth == 0 && containerHeight == 0) {
88 | const ttl = Math.min(retryTTL * 1.5, 500);
89 | setGroupedTimeout(() => this._scaleTextNode(node, ttl), ttl);
90 | return;
91 | }
92 |
93 | // If the tableNode (outer-container) does not have its fontSize set yet,
94 | // we'll set it according to "textSizeRatio"
95 | if (!tableNode.style.fontSize) {
96 | const baseFontSize = containerHeight / textSizeRatio;
97 | tableNode.style.fontSize = `${baseFontSize}px`;
98 | }
99 |
100 | // Reset font-size such that scaling works correctly (#133)
101 | spanNode.style.fontSize = null;
102 |
103 | // Measure the actual width of the text after setting the container size
104 | const { width: textWidth } = node.getBoundingClientRect();
105 |
106 | if (textWidth < 0)
107 | return;
108 |
109 | // Calculate the maximum width for the text based on "textMarginRatio"
110 | const maxTextWidth = containerWidth * (1 - (2 * textMarginRatio));
111 |
112 | // If the text is too wide, scale it down by (maxWidth / actualWidth)
113 | if (textWidth > maxTextWidth)
114 | spanNode.style.fontSize = `calc(1em * ${maxTextWidth / textWidth})`;
115 | }
116 |
117 | render() {
118 | const {
119 | className,
120 | round,
121 | unstyled,
122 | title,
123 | name,
124 | value,
125 | avatar
126 | } = this.props;
127 |
128 | const size = parseSize(this.props.size);
129 |
130 | const initialsStyle = unstyled ? null : {
131 | width: size.str,
132 | height: size.str,
133 | lineHeight: 'initial',
134 | textAlign: 'center',
135 | color: this.props.fgColor,
136 | background: avatar.color,
137 | borderRadius: calculateBorderRadius(round),
138 | };
139 |
140 | const tableStyle = unstyled ? null : {
141 | display: 'table',
142 | tableLayout: 'fixed',
143 | width: '100%',
144 | height: '100%'
145 | };
146 |
147 | const spanStyle = unstyled ? null : {
148 | display: 'table-cell',
149 | verticalAlign: 'middle',
150 | fontSize: '100%',
151 | whiteSpace: 'nowrap'
152 | };
153 |
154 | // Ensure the text node is updated and scaled when any of these
155 | // values changed by calling the `_scaleTextNode` method using
156 | // the correct `ref`.
157 | const key = [
158 | avatar.value,
159 | this.props.size
160 | ].join('');
161 |
162 | return (
163 |
164 |
167 |
168 |
169 |
170 | {avatar.value}
171 |
172 |
173 |
174 |
175 |
176 | );
177 | }
178 |
179 | }
180 |
--------------------------------------------------------------------------------
/src/components/wrapper.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { parseSize, calculateBorderRadius } from '../utils';
5 |
6 | export default
7 | class AvatarWrapper extends React.PureComponent {
8 |
9 | static propTypes = {
10 | className: PropTypes.string,
11 | round: PropTypes.oneOfType([
12 | PropTypes.bool,
13 | PropTypes.string
14 | ]),
15 | style: PropTypes.object,
16 | size: PropTypes.oneOfType([
17 | PropTypes.number,
18 | PropTypes.string
19 | ]),
20 | unstyled: PropTypes.bool,
21 | avatar: PropTypes.object,
22 |
23 | onClick: PropTypes.func,
24 | children: PropTypes.node,
25 | }
26 |
27 | render() {
28 | const {
29 | className,
30 | unstyled,
31 | round,
32 | style,
33 | avatar,
34 | onClick,
35 | children,
36 |
37 | } = this.props;
38 | const { sourceName } = avatar;
39 | const size = parseSize(this.props.size);
40 |
41 | const hostStyle = unstyled ? null : {
42 | display: 'inline-block',
43 | verticalAlign: 'middle',
44 | width: size.str,
45 | height: size.str,
46 | borderRadius: calculateBorderRadius(round),
47 | fontFamily: 'Helvetica, Arial, sans-serif',
48 | ...style
49 | };
50 |
51 | const classNames = [ className, 'sb-avatar' ];
52 |
53 | if (sourceName) {
54 | const source = sourceName.toLowerCase()
55 | .replace(/[^a-z0-9-]+/g, '-') // only allow alphanumeric
56 | .replace(/^-+|-+$/g, ''); // trim `-`
57 | classNames.push('sb-avatar--' + source);
58 | }
59 |
60 | return (
61 |
64 | {children}
65 |
66 | );
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/src/context.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import defaultCache from './cache';
5 | import {defaultColors, defaultInitials} from './utils';
6 |
7 | const defaults = {
8 | cache: defaultCache,
9 | colors: defaultColors,
10 | initials: defaultInitials,
11 | avatarRedirectUrl: null
12 | };
13 |
14 | const contextKeys = Object.keys(defaults);
15 |
16 | /**
17 | * withConfig and ConfigProvider provide a compatibility layer for different
18 | * versions of React equiped with different versions of the Context API.
19 | *
20 | * If the new Context API is available it will be used, otherwise we will
21 | * fall back to the legacy context api.
22 | */
23 |
24 | const ConfigContext = React.createContext && React.createContext();
25 | const isLegacyContext = !ConfigContext;
26 | const ConfigConsumer = isLegacyContext ? null : ConfigContext.Consumer;
27 |
28 | /**
29 | * This was introduced in React 16.3.0 we need this to
30 | * prevent errors in newer versions. But we will just forward the
31 | * component for any version lower than 16.3.0
32 | *
33 | * https://github.com/Sitebase/react-avatar/issues/201
34 | * https://github.com/facebook/react/blob/master/CHANGELOG.md#1630-march-29-2018
35 | */
36 | const forwardRef = React.forwardRef || (C => C);
37 |
38 | export class ConfigProvider extends React.Component {
39 |
40 | static displayName = 'ConfigProvider';
41 |
42 | static propTypes = {
43 | cache: PropTypes.object,
44 | colors: PropTypes.arrayOf(PropTypes.string),
45 | initials: PropTypes.func,
46 | avatarRedirectUrl: PropTypes.string,
47 |
48 | children: PropTypes.node
49 | }
50 |
51 | _getContext() {
52 | const context = {};
53 |
54 | contextKeys.forEach(key => {
55 | if (typeof this.props[key] !== 'undefined')
56 | context[key] = this.props[key];
57 | });
58 |
59 | return context;
60 | }
61 |
62 | render() {
63 | const { children } = this.props;
64 |
65 | if (isLegacyContext)
66 | return React.Children.only(children);
67 |
68 | return (
69 |
70 | {React.Children.only(children)}
71 |
72 | );
73 | }
74 |
75 | }
76 |
77 | export const withConfig = (Component) => {
78 | function withAvatarConfig(props, refOrContext) {
79 |
80 | // If legacy context is enabled, there is no support for forwardedRefs either
81 | if (isLegacyContext) {
82 | const ctx = refOrContext && refOrContext.reactAvatar;
83 | return ( );
84 | }
85 |
86 | /* eslint-disable react/display-name */
87 | return (
88 |
89 | {config => (
90 |
94 | )}
95 |
96 | );
97 | /* eslint-enable react/display-name */
98 | }
99 |
100 | // Legacy support, only set when legacy is detected
101 | withAvatarConfig.contextTypes = ConfigProvider.childContextTypes;
102 |
103 | return forwardRef(withAvatarConfig);
104 | };
105 |
106 | if (isLegacyContext) {
107 | ConfigProvider.childContextTypes = { reactAvatar: PropTypes.object };
108 | ConfigProvider.prototype.getChildContext = function() {
109 | return { reactAvatar: this._getContext() };
110 | };
111 | }
112 |
--------------------------------------------------------------------------------
/src/data-provider.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React, { PureComponent } from 'react';
4 | import PropTypes from 'prop-types';
5 |
6 | import { Cache } from './cache';
7 | import {withConfig, ConfigProvider} from './context';
8 | import InternalState from './internal-state';
9 |
10 | export {getRandomColor} from './utils';
11 | export {ConfigProvider} from './context';
12 | export {Cache} from './cache';
13 |
14 | function matchSource(Source, props, cb) {
15 | const { cache } = props;
16 | const instance = new Source(props);
17 |
18 | if(!instance.isCompatible(props))
19 | return cb();
20 |
21 | instance.get((state) => {
22 | const failedBefore = state && state.src &&
23 | cache.hasSourceFailedBefore(state.src);
24 |
25 | if(!failedBefore && state) {
26 | cb(state);
27 | } else {
28 | cb();
29 | }
30 | });
31 | }
32 |
33 | export default
34 | function createAvatarDataProvider({ sources = [] }) {
35 |
36 | // Collect propTypes for each individual source
37 | const sourcePropTypes = sources.reduce((r, s) => Object.assign(r, s.propTypes), {});
38 |
39 | class AvatarDataProvider extends PureComponent {
40 |
41 | static displayName = 'AvatarDataProvider'
42 |
43 | static propTypes = {
44 | // PropTypes defined on sources
45 | ...sourcePropTypes,
46 |
47 | cache: PropTypes.object,
48 | propertyName: PropTypes.string,
49 | }
50 |
51 | static defaultProps = {
52 | propertyName: 'avatar',
53 | }
54 |
55 | constructor(props) {
56 | super(props);
57 |
58 | this.state = {
59 | internal: null,
60 | src: null,
61 | value: null,
62 | color: props.color,
63 | };
64 | }
65 |
66 | componentDidMount() {
67 | this.fetch();
68 | }
69 |
70 | componentDidUpdate(prevProps) {
71 | let needsUpdate = false;
72 |
73 | // This seems redundant
74 | //
75 | // Props that need to be in `state` are
76 | // `value`, `src` and `color`
77 | for (const prop in sourcePropTypes)
78 | needsUpdate = needsUpdate || (prevProps[prop] !== this.props[prop]);
79 |
80 | if (needsUpdate)
81 | setTimeout(this.fetch, 0);
82 | }
83 |
84 | componentWillUnmount() {
85 | if (this.state.internal) {
86 | this.state.internal.active = false;
87 | }
88 | }
89 |
90 | static Cache = Cache;
91 | static ConfigProvider = ConfigProvider
92 |
93 | _createFetcher = (internal) => (errEvent) => {
94 | const { cache } = this.props;
95 |
96 | if (!internal.isActive(this.state))
97 | return;
98 |
99 | // Mark img source as failed for future reference
100 | if( errEvent && errEvent.type === 'error' )
101 | cache.sourceFailed(errEvent.target.src);
102 |
103 | const pointer = internal.sourcePointer;
104 | if(sources.length === pointer)
105 | return;
106 |
107 | const source = sources[pointer];
108 |
109 | internal.sourcePointer++;
110 |
111 | matchSource(source, this.props, (nextState) => {
112 | if (!nextState)
113 | return setTimeout(internal.fetch, 0);
114 |
115 | if (!internal.isActive(this.state))
116 | return;
117 |
118 | // Reset other values to prevent them from sticking (#51)
119 | nextState = {
120 | src: null,
121 | value: null,
122 | color: null,
123 |
124 | ...nextState
125 | };
126 |
127 | this.setState(state => {
128 | // Internal state has been reset => we received new props
129 | return internal.isActive(state) ? nextState : {};
130 | });
131 | });
132 | }
133 |
134 | fetch = () => {
135 | const internal = new InternalState();
136 | internal.fetch = this._createFetcher(internal);
137 |
138 | this.setState({ internal }, internal.fetch);
139 | };
140 |
141 | render() {
142 | const { children, propertyName } = this.props;
143 | const { src, value, color, sourceName, internal } = this.state;
144 |
145 | const avatarData = {
146 | src,
147 | value,
148 | color,
149 | sourceName,
150 | onRenderFailed: () => internal && internal.fetch() // eslint-disable-line
151 | };
152 |
153 | if (typeof children === 'function')
154 | return children(avatarData);
155 |
156 | const child = React.Children.only(children);
157 | return React.cloneElement(child, {
158 | [propertyName]: avatarData,
159 | });
160 | }
161 | }
162 |
163 | return Object.assign(withConfig(AvatarDataProvider), {
164 | ConfigProvider,
165 | Cache
166 | });
167 | }
168 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import createAvatarComponent from './avatar';
4 | import gravatarSource from './sources/Gravatar';
5 | import facebookSource from './sources/Facebook';
6 | import githubSource from './sources/Github';
7 | import skypeSource from './sources/Skype';
8 | import valueSource from './sources/Value';
9 | import srcSource from './sources/Src';
10 | import iconSource from './sources/Icon';
11 |
12 | // Avatar Redirect
13 | import twitterSource from './sources/Twitter';
14 | import vkontakteSource from './sources/VKontakte';
15 | import instagramSource from './sources/Instagram';
16 | import googleSource from './sources/Google';
17 |
18 | const SOURCES = [
19 | facebookSource,
20 | googleSource,
21 | githubSource,
22 | twitterSource,
23 | instagramSource,
24 | vkontakteSource,
25 | skypeSource,
26 | gravatarSource,
27 | srcSource,
28 | valueSource,
29 | iconSource
30 | ];
31 |
32 | export * from './avatar';
33 | export { default as createAvatarComponent } from './avatar';
34 | export { default as createAvatarDataProvider } from './data-provider';
35 |
36 | export default createAvatarComponent({
37 | sources: SOURCES
38 | });
39 |
40 | export { default as GravatarSource } from './sources/Gravatar';
41 | export { default as FacebookSource } from './sources/Facebook';
42 | export { default as GithubSource } from './sources/Github';
43 | export { default as SkypeSource } from './sources/Skype';
44 | export { default as ValueSource } from './sources/Value';
45 | export { default as SrcSource } from './sources/Src';
46 | export { default as IconSource } from './sources/Icon';
47 |
48 | // Avatar Redirect
49 | export { default as VKontakteSource } from './sources/VKontakte';
50 | export { default as InstagramSource } from './sources/Instagram';
51 | export { default as TwitterSource } from './sources/Twitter';
52 | export { default as GoogleSource } from './sources/Google';
53 | export { default as RedirectSource } from './sources/AvatarRedirect';
54 |
--------------------------------------------------------------------------------
/src/internal-state.js:
--------------------------------------------------------------------------------
1 | export default
2 | class InternalState {
3 |
4 | constructor() {
5 | this.sourcePointer = 0;
6 | this.active = true;
7 | this.fetch = null;
8 | }
9 |
10 | isActive(state = {}) {
11 | // Internal state has been reset => we received new props
12 | if (state.internal !== this)
13 | return false;
14 |
15 | if (!this.fetch)
16 | return false;
17 |
18 | if (this.active !== true)
19 | return false;
20 |
21 | return true;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/sources/AvatarRedirect.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import PropTypes from 'prop-types';
3 | import { getImageSize } from '../utils';
4 |
5 | export default
6 | function createRedirectSource(network, property) {
7 | return class AvatarRedirectSource {
8 |
9 | static propTypes = {
10 | [property]: PropTypes.oneOfType([
11 | PropTypes.string,
12 | PropTypes.number
13 | ])
14 | }
15 |
16 | props = null;
17 |
18 | constructor(props) {
19 | this.props = props;
20 | }
21 |
22 | isCompatible = () => {
23 | return !!this.props.avatarRedirectUrl && !!this.props[property];
24 | }
25 |
26 | get = (setState) => {
27 | const { avatarRedirectUrl } = this.props;
28 | const size = getImageSize(this.props.size);
29 |
30 | const baseUrl = avatarRedirectUrl.replace(/\/*$/, '/');
31 | const id = this.props[property];
32 |
33 | const query = size ? `size=${size}` : '';
34 | const src = `${baseUrl}${network}/${id}?${query}`;
35 |
36 | setState({ sourceName: network, src });
37 | }
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/src/sources/Facebook.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import PropTypes from 'prop-types';
3 | import { getImageSize } from '../utils';
4 |
5 | export default
6 | class FacebookSource {
7 |
8 | static propTypes = {
9 | facebookId: PropTypes.string
10 | }
11 |
12 | props = null;
13 |
14 | constructor(props) {
15 | this.props = props;
16 | }
17 |
18 | isCompatible = () => !!this.props.facebookId
19 |
20 | get = (setState) => {
21 | const { facebookId } = this.props;
22 | const size = getImageSize(this.props.size);
23 |
24 | let url = `https://graph.facebook.com/${facebookId}/picture`;
25 |
26 | if (size)
27 | url += `?width=${size}&height=${size}`;
28 |
29 | setState({
30 | sourceName: 'facebook',
31 | src: url
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/sources/Github.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import PropTypes from 'prop-types';
3 | import { getImageSize } from '../utils';
4 |
5 | export default
6 | class GithubSource {
7 |
8 | static propTypes = {
9 | githubHandle: PropTypes.string
10 | }
11 |
12 | props = null;
13 |
14 | constructor(props) {
15 | this.props = props;
16 | }
17 |
18 | isCompatible = () => !!this.props.githubHandle
19 |
20 | get = (setState) => {
21 | const { githubHandle } = this.props;
22 | const size = getImageSize(this.props.size);
23 |
24 | let url = `https://avatars.githubusercontent.com/${githubHandle}?v=4`;
25 |
26 | if (size)
27 | url += `&s=${size}`;
28 |
29 | setState({
30 | sourceName: 'github',
31 | src: url
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/sources/Google.js:
--------------------------------------------------------------------------------
1 | import redirectSource from './AvatarRedirect';
2 |
3 | export default redirectSource('google', 'googleId');
4 |
--------------------------------------------------------------------------------
/src/sources/Gravatar.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import PropTypes from 'prop-types';
4 | import md5 from 'md5';
5 |
6 | import { getImageSize } from '../utils';
7 |
8 |
9 | export default
10 | class GravatarSource {
11 |
12 | static propTypes = {
13 | email: PropTypes.string,
14 | md5Email: PropTypes.string
15 | }
16 |
17 | props = null;
18 |
19 | constructor(props) {
20 | this.props = props;
21 | }
22 |
23 | isCompatible = () => {
24 | return !!this.props.email || !!this.props.md5Email;
25 | }
26 |
27 | get = (setState) => {
28 | const { props } = this;
29 | const email = props.md5Email || md5(props.email);
30 | const size = getImageSize(props.size);
31 |
32 | let url = `https://secure.gravatar.com/avatar/${email}?d=404`;
33 |
34 | if (size)
35 | url += `&s=${size}`;
36 |
37 | setState({
38 | sourceName: 'gravatar',
39 | src: url
40 | });
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/sources/Icon.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import PropTypes from 'prop-types';
4 | import {getRandomColor} from '../utils';
5 |
6 | export default
7 | class IconSource {
8 |
9 | props = null
10 | icon = '✷'
11 |
12 | static propTypes = {
13 | color: PropTypes.string
14 | }
15 |
16 | constructor(props) {
17 | this.props = props;
18 | }
19 |
20 | isCompatible = () => true
21 |
22 | get = (setState) => {
23 | const { color, colors } = this.props;
24 | setState({
25 | sourceName: 'icon',
26 | value: this.icon,
27 | color: color || getRandomColor(this.icon, colors)
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/sources/Instagram.js:
--------------------------------------------------------------------------------
1 | import redirectSource from './AvatarRedirect';
2 |
3 | export default redirectSource('instagram', 'instagramId');
4 |
--------------------------------------------------------------------------------
/src/sources/Skype.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import PropTypes from 'prop-types';
4 |
5 | export default
6 | class SkypeSource {
7 |
8 | static propTypes = {
9 | skypeId: PropTypes.string
10 | }
11 |
12 | props = null;
13 |
14 | constructor(props) {
15 | this.props = props;
16 | }
17 |
18 | isCompatible = () => !!this.props.skypeId
19 |
20 | get = (setState) => {
21 | const { skypeId } = this.props;
22 | const url = `https://api.skype.com/users/${skypeId}/profile/avatar`;
23 |
24 | setState({
25 | sourceName: 'skype',
26 | src: url
27 | });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/sources/Src.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import PropTypes from 'prop-types';
4 |
5 | export default
6 | class SrcSource {
7 |
8 | static propTypes = {
9 | src: PropTypes.string
10 | }
11 |
12 | props = null
13 |
14 | constructor(props) {
15 | this.props = props;
16 | }
17 |
18 | isCompatible = () => !!this.props.src;
19 |
20 | get = (setState) => {
21 | setState({
22 | sourceName: 'src',
23 | src: this.props.src
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/sources/Twitter.js:
--------------------------------------------------------------------------------
1 | import redirectSource from './AvatarRedirect';
2 |
3 | export default redirectSource('twitter', 'twitterHandle');
4 |
--------------------------------------------------------------------------------
/src/sources/VKontakte.js:
--------------------------------------------------------------------------------
1 | import redirectSource from './AvatarRedirect';
2 |
3 | export default redirectSource('vkontakte', 'vkontakteId');
4 |
--------------------------------------------------------------------------------
/src/sources/Value.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import PropTypes from 'prop-types';
4 | import {getRandomColor, defaultInitials} from '../utils';
5 |
6 | export default
7 | class ValueSource {
8 |
9 | static propTypes = {
10 | color: PropTypes.string,
11 | name: PropTypes.string,
12 | value: PropTypes.string,
13 | email: PropTypes.string,
14 | maxInitials: PropTypes.number,
15 | initials: PropTypes.oneOfType([
16 | PropTypes.string,
17 | PropTypes.func
18 | ])
19 | }
20 |
21 | props = null
22 |
23 | constructor(props) {
24 | this.props = props;
25 | }
26 |
27 | isCompatible = () => {
28 | return !!(this.props.name || this.props.value || this.props.email);
29 | }
30 |
31 | getInitials() {
32 | const { name, initials } = this.props;
33 |
34 | if (typeof initials === 'string')
35 | return initials;
36 |
37 | if (typeof initials === 'function')
38 | return initials(name, this.props);
39 |
40 | return defaultInitials(name, this.props);
41 | }
42 |
43 | getValue() {
44 | if(this.props.name)
45 | return this.getInitials();
46 |
47 | if(this.props.value)
48 | return this.props.value;
49 |
50 | return null;
51 | }
52 |
53 | getColor() {
54 | const {color, colors, name, email, value} = this.props;
55 | const colorValue = name || email || value;
56 | return color || getRandomColor(colorValue, colors);
57 | }
58 |
59 | get = (setState) => {
60 | const value = this.getValue();
61 |
62 | if (!value)
63 | return setState(null);
64 |
65 | setState({
66 | sourceName: 'text',
67 | value: value,
68 | color: this.getColor()
69 | });
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import retina from 'is-retina';
4 |
5 | const IS_RETINA = retina();
6 |
7 | export
8 | function fetch(url, successCb, errorCb) {
9 | const request = new XMLHttpRequest();
10 | request.onreadystatechange = function() {
11 | if (request.readyState === 4) {
12 | if (request.status === 200) {
13 | const data = JSON.parse(request.responseText);
14 | successCb(data);
15 | } else {
16 | errorCb(request.status);
17 | }
18 | }
19 | };
20 | request.open('GET', url, true);
21 | request.send();
22 | }
23 |
24 | export
25 | function fetchJSONP(url, successCb, errorCb) {
26 | const callbackName = 'jsonp_cb_' + Math.round(100000 * Math.random());
27 |
28 | const script = document.createElement('script');
29 | script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'callback=' + callbackName;
30 | document.body.appendChild(script);
31 |
32 | script.onerror = function() {
33 | errorCb();
34 | };
35 |
36 | window[callbackName] = function(data) {
37 | delete window[callbackName];
38 | document.body.removeChild(script);
39 | successCb(data);
40 | };
41 | }
42 |
43 | // https://webaim.org/resources/contrastchecker/
44 | export
45 | const defaultColors = [
46 | '#A62A21',
47 | '#7e3794',
48 | '#0B51C1',
49 | '#3A6024',
50 | '#A81563',
51 | '#B3003C'
52 | ];
53 |
54 | // https://regex101.com/r/YEsPER/1
55 | // https://developer.mozilla.org/en-US/docs/Web/CSS/length
56 | const reSize = /^([-+]?(?:\d+(?:\.\d+)?|\.\d+))([a-z]{2,4}|%)?$/;
57 |
58 | // https://en.wikipedia.org/wiki/Linear_congruential_generator
59 | function _stringAsciiPRNG(value, m) {
60 | // Xn+1 = (a * Xn + c) % m
61 | // 0 < a < m
62 | // 0 <= c < m
63 | // 0 <= X0 < m
64 |
65 | const charCodes = [...value].map(letter => letter.charCodeAt(0));
66 | const len = charCodes.length;
67 |
68 | const a = (len % (m - 1)) + 1;
69 | const c = charCodes.reduce((current, next) => current + next) % m;
70 |
71 | let random = charCodes[0] % m;
72 | for (let i = 0; i < len; i++)
73 | random = ((a * random) + c) % m;
74 |
75 | return random;
76 | }
77 |
78 | export
79 | function getRandomColor(value, colors = defaultColors)
80 | {
81 | // if no value is passed, always return transparent color otherwise
82 | // a rerender would show a new color which would will
83 | // give strange effects when an interface is loading
84 | // and gets rerendered a few consequent times
85 | if(!value)
86 | return 'transparent';
87 |
88 | // value based random color index
89 | // the reason we don't just use a random number is to make sure that
90 | // a certain value will always get the same color assigned given
91 | // a fixed set of colors
92 | const colorIndex = _stringAsciiPRNG(value, colors.length);
93 | return colors[colorIndex];
94 | }
95 |
96 | export
97 | function parseSize(size) {
98 | size = '' + size;
99 |
100 | const [,
101 | value = 0,
102 | unit = 'px'
103 | ] = reSize.exec(size) || [];
104 |
105 | return {
106 | value: parseFloat(value),
107 | str: value + unit,
108 | unit
109 | };
110 | }
111 |
112 | /**
113 | * Calculate absolute size in pixels we want for the images
114 | * that get requested from the various sources. They don't
115 | * understand relative sizes like `em` or `vww`. We select
116 | * a fixed size of 512px when we can't detect the true pixel size.
117 | */
118 | export function getImageSize(size) {
119 | size = parseSize(size);
120 |
121 | if (isNaN(size.value)) // invalid size, use fallback
122 | size = 512;
123 | else if (size.unit === 'px') // px are good, use them
124 | size = size.value;
125 | else if (size.value === 0) // relative 0 === absolute 0
126 | size = 0;
127 | else // anything else is unknown, use fallback
128 | size = 512;
129 |
130 | if (IS_RETINA)
131 | size = size * 2;
132 |
133 | return size;
134 | }
135 |
136 | export
137 | function defaultInitials(name, { maxInitials }) {
138 | return name.split(/\s/)
139 | .map(part => part.substring(0, 1).toUpperCase())
140 | .filter(v => !!v)
141 | .slice(0, maxInitials)
142 | .join('')
143 | .toUpperCase();
144 | }
145 |
146 | /**
147 | * Grouped timeouts reduce the amount of timeouts trigged
148 | * by grouping multiple handlers into a single setTimeout call.
149 | *
150 | * This reduces accuracy of the timeout but will be less expensive
151 | * when multiple avatar have been loaded into view.
152 | */
153 | const timeoutGroups = {};
154 |
155 | export
156 | function setGroupedTimeout(fn, ttl) {
157 | if (timeoutGroups[ttl]) {
158 | timeoutGroups[ttl].push(fn);
159 | return;
160 | }
161 |
162 | const callbacks = timeoutGroups[ttl] = [fn];
163 | setTimeout(() => {
164 | delete timeoutGroups[ttl];
165 | callbacks.forEach(cb => cb());
166 | }, ttl);
167 | }
168 |
169 | export
170 | function getNullableText(...args) {
171 | for (const arg of args) {
172 | if (arg || arg === '')
173 | return arg;
174 |
175 | if (arg === false || arg === null)
176 | return null;
177 | }
178 |
179 | return;
180 | }
181 |
182 | export
183 | function calculateBorderRadius(round) {
184 | if (round === true)
185 | return '100%';
186 |
187 | if (round === false)
188 | return;
189 |
190 | return round;
191 | }
192 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "jsx": "react",
6 | "noEmit": true,
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | const path = require('path');
4 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
5 |
6 | const rel = (p) => path.resolve(__dirname, p);
7 | const isDev = (process.env.NODE_ENV !== 'production');
8 | const plugins = [];
9 |
10 | if (!isDev) {
11 | plugins.push(new BundleAnalyzerPlugin({
12 | analyzerMode: 'static',
13 | openAnalyzer: false
14 | }));
15 | }
16 |
17 | module.exports = {
18 | mode: isDev ? 'development' : 'production',
19 |
20 | entry: './demo',
21 |
22 | output: {
23 | path: rel('build'),
24 | filename: 'demo.js'
25 | },
26 |
27 | module: {
28 | rules: [{
29 | enforce: 'pre',
30 | test: /\.js$/,
31 | use: 'eslint-loader'
32 | }, {
33 | test: /\.js$/,
34 | exclude: /node_modules/,
35 | use: 'babel-loader'
36 | }, {
37 | test: /\.(css|html|jpe?g|png)$/,
38 | use: [{
39 | loader: 'file-loader',
40 | options: {
41 | name: '[name].[ext]'
42 | }
43 | }]
44 | }]
45 | },
46 |
47 | devServer: {
48 | static: {
49 | directory: rel('build'),
50 | },
51 | compress: true,
52 | port: 8000
53 | },
54 |
55 | watchOptions: {
56 | aggregateTimeout: 500
57 | },
58 |
59 | plugins: [
60 | ...plugins
61 | ]
62 | };
63 |
--------------------------------------------------------------------------------