├── .DS_Store ├── .dependabot └── config.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── RELEASE-NOTES.md ├── dist ├── reactBundle.js └── widget.js ├── example ├── .DS_Store ├── index.html ├── profile-hover.gif ├── react │ ├── app.js │ ├── bundle.js │ └── index.html └── server.js ├── package-lock.json ├── package.json ├── src ├── CopyButton.jsx ├── CopyButton_react.jsx ├── ProfileHover.jsx ├── html.jsx ├── style.less ├── utils.js └── widget.js ├── webpack.config.js └── webpack.example.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3box/profile-hover/64ccb0880023ae72289741dca54a308d9fcabcbf/.DS_Store -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | - package_manager: "javascript" 4 | target_branch: "develop" 5 | directory: "/" 6 | update_schedule: "weekly" 7 | default_reviewers: 8 | - "zachferland" 9 | default_assignees: 10 | - "zachferland" 11 | allowed_updates: 12 | - match: 13 | dependency_type: "production" 14 | update_type: "all" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /example 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 3Box 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 | # ⚠️ ⚠️ This project is no longer supported ⚠️ ⚠️ 2 | > 3box.js and related tools built by 3Box Labs are deprecated and no loger supported. Developers are encurraged to build with https://ceramic.network which is a more secure and decentralized protocol for sovereign data. 3 | 4 | # Profile Hover 5 | 6 | `profile-hover` is a drop-in component that displays profile metadata for any ethereum address. Available in React and HTML/CSS versions. 7 | 8 | ![Profile Hover](./example/profile-hover.gif) 9 | 10 | 11 | ## Component Overview 12 | The Profile Hover consists of two components: the `Tile`, which is displayed on the page, and the `Hover`, which is displayed when the Tile is hovered. Profile Hover is available for React and HTML/CSS apps. 13 | 14 | ## Getting Started 15 | 16 | ### React Component 17 | Installation: 18 | 19 | ```shell 20 | npm i -S profile-hover 21 | ``` 22 | 23 | Usage: 24 | 25 | ```jsx 26 | import ProfileHover from 'profile-hover'; 27 | 28 | const MyComponent = () => (); 29 | ``` 30 | 31 | ### HTML Element 32 | 33 | First add the script at the end of your page. 34 | 35 | ```html 36 | 37 | ``` 38 | 39 | Then add the following tag where ever you display an address. 40 | 41 | ```html 42 | 43 | ``` 44 | 45 | Additional Options: 46 | 47 | Add `data-display='full'` to show the entire address instead of the shorten display. 48 | 49 | ```html 50 | 51 | ``` 52 | 53 | Add `data-theme='none'` to not use any of our address bar styling. Allows you to wrap any existing elements in an address hover. 54 | 55 | ```html 56 | 57 | ... your own html and styling 58 | 59 | ``` 60 | 61 | ## How to Customize 62 | Here are the ways you can customize the profile hover to better suit your app's needs. 63 | 64 | ### Prop Types 65 | 66 | | Property | Type | Default | Component | Description | 67 | | :-------------------------------- | :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 68 | | `address` | String | | | `Address` property value is **required** to work. Provide an Ethereum address to this property to fetch profile information. | 69 | | `showName` | Boolean | False | Tile | Provide property `showName` to show the user's name from their 3Box profile instead of their Ethereum address.| 70 | | `url` | String | | Tile | Provide property `url` with url string to set where clicking on the Tile will redirect the user.| 71 | | `displayFull` | Boolean | False | Tile | Add `displayFull` property to show the entire address instead of the shortened display.| 72 | | `tileStyle` | Boolean | False | Tile | Add `tileStyle` property to render the tile component as a tile. | 73 | | `noTheme` | Boolean | False | Tile | Add `noTheme` property to not use any of our Tile styling. Allows you to wrap any existing elements in a Hover component. | 74 | | `noImgs` | Boolean | False | Hover | Add `noImgs` property to prevent displaying of profile image and cover image in the Hover. | 75 | | `noProfileImg` | Boolean | False | Hover | Add `noProfileImg` property to prevent displaying of just the profile image in the Hover. | 76 | | `noCoverImg` | Boolean | False | Hover | Add `noCoverImg` property to prevent displaying of just the cover image in the Hover. | 77 | | `orientation` | String | `'right'` | Hover | Provide property `orientation` with string `'top'`, `'bottom'`, `'right'` or `'left'` to set which way the Hover will pop up from the Tile.| 78 | 79 | #### Prop Types example 80 | ```jsx 81 | 87 | ``` 88 | ```jsx 89 | 93 | ... your own html and styling 94 | 95 | ``` 96 | 97 | ## Differences Between Desktop and Mobile 98 | Given the current state of Web3 mobile dapp browsers and their lack of browser tab support, the behavior of the profile-hover React component has minor differences depending on device context. On desktop web and web2 mobile browsers, out-bound links within the hover element work as usual and open a new tab. However on Web3 mobile dapp browsers, since tabs do not exist, clicking on a link within the hover component will, instead, copy that URL to a users clipboard. 99 | 100 | ## Maintainers 101 | [@zachferland](https://github.com/zachferland) 102 | -------------------------------------------------------------------------------- /RELEASE-NOTES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v1.1.2 - 2020-03-09 4 | 5 | * chore: update to 3box@1.17.1 6 | 7 | ## v1.1.1 - 2019-09-13 8 | 9 | * fix: return null for cover image if there isn't one 10 | * fix: more css rules for varying edge cases 11 | 12 | ## v1.0.2 - 2019-05-31 13 | 14 | * fix: align hover arrow 15 | * feat: adds option to display name instead of addess in tile 16 | * feat: additional tile style option and changes default 17 | * fix: catch missing cover img error 18 | 19 | ## v1.0.1 - 2019-05-30 20 | * Fix(CSS): tile sizing in mobile 21 | 22 | ## v1.0.0 - 2019-05-29 23 | * Support in React with a React component :tada: :zap: Huge thanks to @raksooo for making this happen! 24 | * A new redesigned hover and address bar. Along with much improved mobile support and new configuration options. Nice work by @oznekenzo :fire: 25 | 26 | ## v0.2.3 - 2019-04-26 27 | * Fix: Handle undefined website field 28 | -------------------------------------------------------------------------------- /example/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3box/profile-hover/64ccb0880023ae72289741dca54a308d9fcabcbf/example/.DS_Store -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 3box Hover Demo 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 | 34 |
35 | 36 |
37 | 38 | 39 | 0xa8ee0babe72cd9a80ae45dd74cd3eae7a82fd5d1 40 | 41 | 42 |






















43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/profile-hover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3box/profile-hover/64ccb0880023ae72289741dca54a308d9fcabcbf/example/profile-hover.gif -------------------------------------------------------------------------------- /example/react/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ProfileHover from '../../dist/reactBundle'; 4 | 5 | const Example = ({}) => { 6 | return ( 7 | <> 8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 | 0xa8ee0babe72cd9a80ae45dd74cd3eae7a82fd5d1 32 | 33 |
34 | 35 | ) 36 | } 37 | 38 | const appContainer = document.getElementById('reactApp'); 39 | ReactDOM.render(, appContainer); 40 | -------------------------------------------------------------------------------- /example/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 3box Hover Demo 5 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const serveStatic = require('serve-static') 3 | const path = require('path') 4 | const port = 30000 5 | 6 | const app = express() 7 | app.use(serveStatic(path.join(__dirname), {'index': ['index.html']})) 8 | // This allows the dist/3Box.js relative path to be resolved 9 | app.use(serveStatic(path.join(__dirname, '../'))) 10 | app.listen(port, () => { 11 | console.log(`Open http://localhost:${port} in a browser to start using the example`) 12 | }) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "profile-hover", 3 | "version": "1.1.2", 4 | "description": "Drop in profile hover for any ethereum address", 5 | "main": "dist/reactBundle.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build:dist": "./node_modules/.bin/webpack --config webpack.config.js --mode=development", 9 | "build:dist:prod": "./node_modules/.bin/webpack --config webpack.config.js --mode=production", 10 | "build:example": "./node_modules/.bin/webpack --config webpack.example.js --mode=development", 11 | "example:start": "node example/server.js | npm run build:dist" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/3box/profile-hover.git" 16 | }, 17 | "author": "3box.io", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/3box/profile-hover/issues" 21 | }, 22 | "contributors": [ 23 | "Zach Ferland ", 24 | "Oskar Nyberg ", 25 | "Kenzo Nakamura " 26 | ], 27 | "homepage": "https://github.com/3box/profile-hover#readme", 28 | "dependencies": { 29 | "3box": "^1.17.1", 30 | "@fortawesome/fontawesome-svg-core": "^1.2.15", 31 | "@fortawesome/free-brands-svg-icons": "^5.7.2", 32 | "@fortawesome/free-regular-svg-icons": "^5.7.2", 33 | "@fortawesome/free-solid-svg-icons": "^5.7.2", 34 | "@fortawesome/react-fontawesome": "^0.1.4", 35 | "ethereum-blockies-base64": "^1.0.2", 36 | "jsx-render": "^1.1.2", 37 | "store": "^2.0.12" 38 | }, 39 | "devDependencies": { 40 | "@babel/cli": "^7.1.2", 41 | "@babel/core": "^7.1.2", 42 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 43 | "@babel/plugin-transform-modules-commonjs": "^7.2.0", 44 | "@babel/plugin-transform-runtime": "^7.1.0", 45 | "@babel/preset-env": "^7.1.0", 46 | "@babel/preset-react": "^7.0.0", 47 | "babel-core": "7.0.0-bridge.0", 48 | "babel-loader": "^8.0.5", 49 | "css-loader": "^2.1.0", 50 | "css-to-string-loader": "^0.1.3", 51 | "express": "^4.16.4", 52 | "ganache-cli": "^6.1.0", 53 | "ipfsd-ctl": "^0.40.1", 54 | "jest": "^23.6.0", 55 | "jsdoc-to-markdown": "^4.0.1", 56 | "less": "^3.9.0", 57 | "less-loader": "^4.1.0", 58 | "react": "^16.8.6", 59 | "react-dom": "^16.8.6", 60 | "standard": "^12.0.1", 61 | "style-loader": "^0.23.1", 62 | "webpack": "^4.20.2", 63 | "webpack-cli": "^3.1.2" 64 | }, 65 | "peerDependencies": { 66 | "react": ">=16.3.0", 67 | "react-dom": ">=16.3.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/CopyButton.jsx: -------------------------------------------------------------------------------- 1 | import dom from 'jsx-render'; 2 | const style = require('style-loader!./style.less'); 3 | 4 | export const CopyButton = ({ address }) => { 5 | const onClick = `boxCopyAddress_f1kx(this, '${address}')`; 6 | return ( 7 |
8 |
9 | Wallet 10 |

11 | {`${address.substr(0, 5)}...${address.substr(-5)}`} 12 |

13 | 14 |
15 |
16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/CopyButton_react.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck" 4 | import { addToClipboard } from './utils'; 5 | const style = require('style-loader!./style.less') 6 | 7 | export class CopyButton extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | showCheck: false 13 | }; 14 | } 15 | 16 | _onClick() { 17 | addToClipboard(this.props.address); 18 | this.setState({ showCheck: true }); 19 | setTimeout(() => { 20 | this.setState({ showCheck: false }); 21 | }, 2000); 22 | } 23 | 24 | render() { 25 | const icon = this.state.showCheck ? faCheck : null; 26 | return ( 27 |
28 |
this._onClick()} > 29 | Wallet 30 |

31 | {`${this.props.address.substr(0, 5)}...${this.props.address.substr(-5)}`} 32 |

33 |
34 | {icon && } 35 |
36 | ); 37 | } 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /src/ProfileHover.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { getProfile, getVerifiedAccounts } from "3box/lib/api"; 3 | import { getAddressDisplay, formatProfileData, formatUrl, checkIsMobile } from './utils'; 4 | const { BaseTemplate, LoadingTemplate } = require('./html')({ React, Fragment }); 5 | 6 | export default class ProfileHover extends React.PureComponent { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | profile: undefined, 11 | verified: undefined, 12 | adjustOrientation: undefined, 13 | isMobile: false, 14 | showHover: false, 15 | hasWeb3Mobile: false, 16 | copySuccessful: '' 17 | }; 18 | this.selector = React.createRef(); 19 | this.checkWindowSize = this.checkWindowSize.bind(this); 20 | this.handleShowHover = this.handleShowHover.bind(this); 21 | this.handleCopySuccessful = this.handleCopySuccessful.bind(this); 22 | this.checkHasWeb3Mobile = this.checkHasWeb3Mobile.bind(this); 23 | } 24 | 25 | componentDidCatch(error, info) { 26 | console.error({ error, info }); 27 | } 28 | 29 | componentDidMount() { 30 | this.setState({ isMobile: checkIsMobile() }); 31 | this.checkHasWeb3Mobile(); 32 | this._fetchProfile(); 33 | } 34 | 35 | componentDidUpdate(prevProps) { 36 | if (this.props.address !== prevProps.address) { 37 | this._fetchProfile(); 38 | } 39 | } 40 | 41 | async _fetchProfile() { 42 | try { 43 | const profile = await getProfile(this.props.address); 44 | const verified = await getVerifiedAccounts(profile); 45 | this.setState({ profile, verified, hasUpdated: false }); 46 | } catch (error) { 47 | console.error('3box profile fetch failed', error); 48 | } 49 | } 50 | 51 | checkWindowSize(isNotMobile) { 52 | try { 53 | const { hasUpdated } = this.state; 54 | if (!hasUpdated && isNotMobile) { 55 | const { adjustOrientation } = this.state; 56 | const height = window.innerHeight; 57 | const rect = this.selector.current.getBoundingClientRect(); 58 | const elHeight = rect.height; 59 | const elY = rect.y; 60 | 61 | let updateOrientation = adjustOrientation; 62 | 63 | if (elHeight + elY > height) { 64 | updateOrientation = 'top'; 65 | } else if (elY < 0) { 66 | updateOrientation = 'bottom'; 67 | } 68 | 69 | this.setState({ adjustOrientation: updateOrientation, hasUpdated: true }); 70 | } 71 | } catch (error) { 72 | console.error(error); 73 | } 74 | }; 75 | 76 | checkHasWeb3Mobile() { 77 | const hasWeb3Mobile = checkIsMobile() && ( 78 | typeof window.web3 !== 'undefined' || typeof window.ethereum !== 'undefined'); 79 | this.setState({ hasWeb3Mobile }); 80 | }; 81 | 82 | handleShowHover(isMobile) { 83 | const { showHover, hasUpdated } = this.state; 84 | if (isMobile) this.setState({ showHover: !showHover }, 85 | () => { if (!hasUpdated) this.checkWindowSize(true) }); 86 | } 87 | 88 | handleCopySuccessful(field) { 89 | this.setState({ copySuccessful: field }, 90 | () => setTimeout(() => { 91 | this.setState({ copySuccessful: '' }); 92 | }, 2000) 93 | ); 94 | } 95 | 96 | render() { 97 | const { 98 | address, 99 | fullDisplay, 100 | children, 101 | noTheme, 102 | noImgs, 103 | noProfileImg, 104 | noCoverImg, 105 | orientation, 106 | url, 107 | showName, 108 | tileStyle 109 | } = this.props; 110 | 111 | const { 112 | profile, 113 | verified, 114 | adjustOrientation, 115 | isMobile, 116 | showHover, 117 | hasWeb3Mobile, 118 | copySuccessful 119 | } = this.state; 120 | 121 | if (address == null) { 122 | return null; 123 | } 124 | 125 | const opts = { 126 | html: noTheme ? children : undefined, 127 | noImgs, 128 | noProfileImg, 129 | noCoverImg, 130 | showName, 131 | tileStyle, 132 | url: formatUrl(url), 133 | orientation: adjustOrientation || orientation || 'right', 134 | }; 135 | 136 | const addressDisplay = getAddressDisplay(address, fullDisplay ? 'full' : undefined) 137 | const data = formatProfileData(profile, verified, address, addressDisplay); 138 | 139 | if (profile == null) { 140 | return ; 149 | } 150 | 151 | if (profile.status === 'error') { 152 | return ; 162 | } 163 | 164 | return ; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/html.jsx: -------------------------------------------------------------------------------- 1 | const { CopyButton } = require('./CopyButton'); 2 | const { addToClipBoardLinks } = require('./utils'); 3 | const { FontAwesomeIcon } = require('@fortawesome/react-fontawesome'); 4 | const { faCheck } = require('@fortawesome/free-solid-svg-icons/faCheck'); 5 | const style = require('style-loader!./style.less'); 6 | 7 | module.exports = ({ dom, React, Fragment }) => { 8 | let BaseTemplate = 9 | ({ 10 | data = {}, 11 | opts = {}, 12 | checkWindowSize, 13 | showHover, 14 | isMobile, 15 | handleShowHover, 16 | hasWeb3Mobile, 17 | handleCopySuccessful, 18 | copySuccessful 19 | }, 20 | ref) => { 21 | return ( 22 |
checkWindowSize(!isMobile)} 25 | > 26 | {opts.html ? ( 27 | 28 | {opts.html} 29 | 30 | 39 | 40 | ) : ( 41 | 52 | )} 53 | 54 | {showHover &&
handleShowHover(true)} />} 55 |
56 | ); 57 | } 58 | if (React) BaseTemplate = React.forwardRef(BaseTemplate) 59 | 60 | let HoverTemplate = 61 | ({ 62 | data = {}, 63 | opts = {}, 64 | showHover, 65 | hasWeb3Mobile, 66 | handleCopySuccessful, 67 | copySuccessful, 68 | loading 69 | }, 70 | ref) => { 71 | return ( 72 |
73 |
74 | {loading &&
Loading ...
} 75 | 76 | {data.coverPhoto && } 77 | {data.imgSrc && } 78 | 79 | {data.name && } 85 | 86 | {data.description && } 87 | 88 | {(data.twitter || data.github || data.website) && ( 89 |
90 | {data.twitter && } 96 | 97 | {data.github && } 103 | 104 | {data.website && } 110 |
)} 111 | 112 | 118 |
119 |
120 | ); 121 | } 122 | if (React) HoverTemplate = React.forwardRef(HoverTemplate) 123 | 124 | const HoverFooterTemplate = ({ data = {}, hasWeb3Mobile, handleCopySuccessful, copySuccessful }) => ( 125 |
126 | 127 | 128 | {hasWeb3Mobile 129 | ? 130 | : } 131 | 132 | {copySuccessful === 'footer' && } 133 |
134 | ); 135 | 136 | const HoverFooterWeb3MobileLink = ({ data, handleCopySuccessful }) => ( 137 |

addToClipBoardLinks( 139 | `https://3box.io/${data.address}`, 140 | handleCopySuccessful, 141 | 'footer' 142 | )} 143 | className={style.boxLinkText} 144 | > 145 | View 3Box 146 | 147 |

148 | ); 149 | 150 | const HoverFooterLink = ({ data }) => ( 151 | 157 | View 3Box 158 | 159 | 160 | ); 161 | 162 | const CoverPictureTemplate = ({ data = {}, opts = {} }) => { 163 | return ( 164 | 165 | { 166 | (!opts.noCoverImg && !opts.noImgs) && ( 167 |
168 | {data.coverPhoto ? 169 | :
} 170 |
) 171 | } 172 | 173 | ) 174 | } 175 | 176 | let AddressBarTemplate = 177 | ({ 178 | data = {}, 179 | opts = {}, 180 | showHover, 181 | isMobile, 182 | handleShowHover, 183 | hasWeb3Mobile, 184 | handleCopySuccessful, 185 | copySuccessful, 186 | loading 187 | }, 188 | ref 189 | ) => { 190 | return ( 191 |
= 15 ? style.boxAddressFull : ''} ${opts.tileStyle ? style.tileStyle : ''}`} 193 | > 194 |
{ if (opts.url) window.location = `${opts.url}`; }} 197 | > 198 |
199 | {data.imgSrc && } 200 |
201 | 202 |
203 | {(opts.showName && data.name) ? data.name : data.addressDisplay} 204 |
205 |
206 | 207 | {isMobile && ( 208 | )} 214 | 215 | 225 |
226 | ) 227 | } 228 | if (React) AddressBarTemplate = React.forwardRef(AddressBarTemplate) 229 | 230 | const ProfilePictureTemplate = ({ data = {}, opts = {} }) => { 231 | return ( 232 | 233 | {(!opts.noProfileImg && !opts.noImgs) && ( 234 |
235 | 236 |
237 | )} 238 | {(opts.noProfileImg && !opts.noImgs) && (
)} 239 | ) 240 | } 241 | 242 | const DescriptionTemplate = ({ data = {} }) => ( 243 |
244 |

{data.description}

245 |
246 | ) 247 | 248 | const NameTemplate = ({ data = {}, hasWeb3Mobile, handleCopySuccessful, copySuccessful }) => ( 249 |
255 | {hasWeb3Mobile ? ( 256 |

addToClipBoardLinks( 258 | `https://3box.io/${data.address}`, 259 | handleCopySuccessful, 260 | 'threeBoxProfile' 261 | )} 262 | className={style.profileText}> 263 | {data.name} 264 |

265 | ) : ( 271 | {data.name} 272 | )} 273 | {data.emoji && {data.emoji}} 274 | 275 | {copySuccessful === 'threeBoxProfile' && } 276 |
277 | ) 278 | 279 | const WebsiteTemplate = ({ data = {}, hasWeb3Mobile, handleCopySuccessful, copySuccessful }) => ( 280 |
281 |

Website

282 | 283 | {hasWeb3Mobile ? ( 284 | 290 | ) : ( 291 | {data.website} 292 | )} 293 | 294 | {copySuccessful === 'website' && } 295 |
296 | ) 297 | 298 | const GithubTemplate = ({ data = {}, hasWeb3Mobile, handleCopySuccessful, copySuccessful }) => ( 299 |
300 |

Github

301 | 302 | {hasWeb3Mobile ? ( 303 | 309 | ) : ( 310 | {data.github} 311 | )} 312 | 313 | {copySuccessful === 'github' && } 314 |
315 | ) 316 | 317 | const TwitterTemplate = ({ data = {}, hasWeb3Mobile, copySuccessful, handleCopySuccessful }) => ( 318 |
319 |

Twitter

320 | 321 | {hasWeb3Mobile ? ( 322 | 328 | ) : 329 | {`@${data.twitter}`} 330 | } 331 | 332 | {copySuccessful === 'twitter' && } 333 |
334 | ); 335 | 336 | const SocialWeb3Link = ({ handleCopySuccessful, link, field, handle }) => ( 337 |

addToClipBoardLinks(link, handleCopySuccessful, field)}> 338 | {handle} 339 |

340 | ); 341 | 342 | let LoadingTemplate = ({ data = {}, opts = {}, showHover, isMobile, checkWindowSize }, ref) => { 343 | return ( 344 |
checkWindowSize(!isMobile)} 347 | > 348 | {opts.html ? ( 349 | 350 |
{opts.html}
351 | 358 |
359 | ) : ( 360 | )} 367 |
368 | ) 369 | } 370 | if (React) LoadingTemplate = React.forwardRef(LoadingTemplate) 371 | 372 | return { 373 | BaseTemplate, 374 | LoadingTemplate, 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/style.less: -------------------------------------------------------------------------------- 1 | @threeBoxBlack: #181F21; 2 | @lighterFont: #949494; 3 | @lightestFont: #ababab; 4 | @coverHeight: 70px; 5 | @baseTileHeight: 50px; 6 | 7 | .boxAddress { 8 | background: transparent; 9 | height: fit-content; 10 | padding: 0px; 11 | font-family: Arial, sans-serif; 12 | position: relative; 13 | display: block; 14 | width: fit-content; 15 | display: flex; 16 | justify-content: flex-start; 17 | align-items: center; 18 | } 19 | 20 | .boxAddress.tileStyle { 21 | border: 1px solid rgb(237, 242, 251); 22 | background: white; 23 | border-radius: 5px; 24 | } 25 | 26 | .boxAddressContentWrapper { 27 | display: flex; 28 | justify-content: flex-end; 29 | align-items: center; 30 | } 31 | 32 | .boxAddressContentWrapper.boxAddressLink { 33 | cursor: pointer; 34 | } 35 | 36 | .tileStyle .boxAddressContentWrapper { 37 | max-width: calc(200px + @baseTileHeight + 40px); 38 | min-width: calc(92px + @baseTileHeight + 40px); 39 | } 40 | 41 | .boxAddressContentWrapper { 42 | max-width: calc(200px + @baseTileHeight + 40px); 43 | } 44 | 45 | .boxAddressFull { 46 | width: fit-content; 47 | } 48 | 49 | .boxAddressWrap { 50 | display: block; 51 | float: left; 52 | position: relative; 53 | font-family: Arial, sans-serif; 54 | -webkit-font-smoothing: antialiased; 55 | background-clip: padding-box; 56 | } 57 | 58 | .boxAddress .boxImg { 59 | background: rgb(7, 73, 136); 60 | height: @baseTileHeight; 61 | width: @baseTileHeight; 62 | border-radius: 50%; 63 | overflow: hidden; 64 | } 65 | 66 | .boxAddress.tileStyle .boxImg { 67 | border-radius: 5px 0px 0px 5px; 68 | } 69 | 70 | .boxAddress .boxImg img { 71 | height: 100%; 72 | width: 100%; 73 | object-fit: cover; 74 | background-color: white; 75 | } 76 | 77 | .boxShortAddress { 78 | color: rgb(99, 102, 104); 79 | font-size: 15px; 80 | font-weight: 600; 81 | letter-spacing: 0.015em; 82 | display: block; 83 | padding: 0 16px; 84 | max-width: 200px; 85 | text-overflow: ellipsis; 86 | overflow: hidden; 87 | white-space: nowrap; 88 | } 89 | 90 | .tileStyle .boxShortAddress { 91 | min-width: 100px; 92 | } 93 | 94 | .openHover_mobile { 95 | width: fit-content; 96 | height: fit-content; 97 | color: #b7bece; 98 | padding: 0; 99 | border: none; 100 | background-color: transparent; 101 | } 102 | 103 | .tileStyle .openHover_mobile { 104 | background-color: rgb(249, 249, 249); 105 | border-left: 1px solid rgb(237, 242, 251); 106 | border-radius: 0 5px 5px 0; 107 | width: @baseTileHeight; 108 | height: @baseTileHeight; 109 | } 110 | 111 | .openHover_mobile svg { 112 | height: 14px !important; 113 | width: 14px !important; 114 | } 115 | 116 | .addressCopy { 117 | position: relative; 118 | color: #b7bece; 119 | border-left: 1px solid #edf2fb; 120 | padding: 9px 4px 4px 9px; 121 | width: 16px; 122 | font-size: 12px; 123 | height: 20px; 124 | display: inline-block; 125 | } 126 | 127 | .addressCopy:hover { 128 | cursor: pointer; 129 | color: #7c8aa7; 130 | } 131 | 132 | .addressCopy:active { 133 | background-color: #f7f7fb; 134 | } 135 | 136 | .addressAndCheck svg { 137 | margin-left: 6px; 138 | } 139 | 140 | .boxAddressWrap .hoverWrap { 141 | height: fit-content; 142 | width: fit-content; 143 | position: absolute; 144 | z-index: 2; 145 | } 146 | 147 | .boxAddressWrap .hoverWrap.top { 148 | padding-bottom: 20px; 149 | bottom: 90%; 150 | left: -10px; 151 | } 152 | 153 | .boxAddressWrap .hoverWrap.bottom { 154 | padding-top: 20px; 155 | top: 90%; 156 | left: -10px; 157 | } 158 | 159 | .boxAddressWrap .hoverWrap.right { 160 | padding-left: 20px; 161 | top: calc(50% - 33px); 162 | left: calc(100% + -5px); 163 | } 164 | 165 | .boxAddressWrap .hoverWrap.left { 166 | padding-right: 20px; 167 | top: calc(50% - 33px); 168 | right: calc(100% + -5px); 169 | } 170 | 171 | .boxAddressWrap .hoverProfile { 172 | border-radius: 10px; 173 | border: 1px solid rgb(237, 242, 251); 174 | box-shadow: 0 3px 8px 0 rgba(87, 87, 87, 0.15); 175 | background: white; 176 | color: @threeBoxBlack; 177 | width: 230px; 178 | z-index: 1111; 179 | display: none; 180 | flex-direction: column; 181 | justify-content: flex-start; 182 | align-items: flex-start; 183 | padding: 20px 20px 20px 20px; 184 | position: relative; 185 | } 186 | 187 | .boxAddressWrap.isDesktop:hover .hoverWrap .hoverProfile { 188 | display: flex; 189 | } 190 | 191 | .boxAddressWrap .hoverWrap.showHoverMobile .hoverProfile { 192 | display: flex; 193 | box-sizing: border-box; 194 | min-width: 270px; 195 | } 196 | 197 | .hoverProfileEmpty { 198 | padding: 20px; 199 | } 200 | 201 | .hoverProfile p { 202 | letter-spacing: .4px; 203 | } 204 | 205 | .hoverProfile textarea { 206 | font-size: 18px; 207 | } 208 | 209 | .hoverProfile .coverPicture { 210 | position: absolute; 211 | top: 0; 212 | width: 100%; 213 | height: @coverHeight; 214 | z-index: 1; 215 | margin-left: -20px; 216 | } 217 | 218 | .hoverProfile .coverPicture_image { 219 | min-width: 100%; 220 | min-height: @coverHeight; 221 | max-width: 100%; 222 | max-height: @coverHeight; 223 | object-fit: cover; 224 | border-top-left-radius: 10px; 225 | border-top-right-radius: 10px; 226 | background-color: #f5f5f5; 227 | } 228 | 229 | .hoverProfile .profileValue { 230 | display: flex; 231 | justify-content: flex-start; 232 | align-items: center; 233 | width: 100%; 234 | height: 18px; 235 | } 236 | 237 | .hoverProfile .profileValue a { 238 | text-decoration: none; 239 | color: rgb(72, 125, 236); 240 | font-size: 14px; 241 | width: 80%; 242 | white-space: nowrap; 243 | } 244 | 245 | .hoverProfile p { 246 | margin: 0; 247 | color: @lightestFont; 248 | } 249 | 250 | .hoverProfile .profileValuePicture { 251 | z-index: 2; 252 | margin-top: 10px; 253 | display: flex; 254 | } 255 | 256 | .hoverProfile .profileValuePicture .profileValuePicture_image { 257 | background: white; 258 | height: 60px; 259 | width: 60px; 260 | border-radius: 50%; 261 | object-fit: cover; 262 | margin-bottom: 14px; 263 | border: 3px solid white; 264 | } 265 | 266 | .hoverProfile .noProfileImgSpacer { 267 | margin-top: 65px; 268 | } 269 | 270 | .hoverProfile .profileDescription .profileDescription_text { 271 | font-size: 14px; 272 | margin-top: 4px; 273 | color: @lighterFont; 274 | line-height: 18px; 275 | text-align: left; 276 | } 277 | 278 | .hoverProfile .profileValue .profileText { 279 | padding-left: 6px; 280 | display: flex; 281 | justify-content: flex-start; 282 | align-items: center; 283 | color: rgb(72, 125, 236); 284 | } 285 | 286 | .hoverProfile .profileValue svg { 287 | margin-left: 8px; 288 | color: rgb(72, 125, 236); 289 | } 290 | 291 | .hoverProfile .profileValueName { 292 | letter-spacing: 1px; 293 | width: 100%; 294 | overflow: hidden; 295 | display: flex; 296 | justify-content: flex-start; 297 | align-items: center; 298 | } 299 | 300 | .hoverProfile .profileValueName.noContent { 301 | margin-bottom: 20px; 302 | } 303 | 304 | .hoverProfile .profileValueName span { 305 | margin-left: 5px; 306 | } 307 | 308 | .hoverProfile .profileValueName svg { 309 | margin-left: 8px; 310 | } 311 | 312 | .hoverProfile .profileValueName .profileText { 313 | text-decoration: none; 314 | color: @threeBoxBlack; 315 | font-weight: 600; 316 | font-size: 20px; 317 | text-align: left; 318 | } 319 | 320 | .hoverProfile .profileText p { 321 | color: #487dec !important; 322 | font-size: 14px !important; 323 | } 324 | 325 | .hoverProfile .profileValueName .profileText:hover { 326 | text-decoration: underline; 327 | } 328 | 329 | .hoverProfile .profileValueName .profileText:visited { 330 | color: @threeBoxBlack; 331 | } 332 | 333 | .hoverProfile .boxLink { 334 | font-size: 12px; 335 | border-radius: 0px 0px 3px 3px; 336 | color: @lightestFont; 337 | display: flex; 338 | justify-content: space-between; 339 | align-items: center; 340 | width: 100%; 341 | position: relative; 342 | } 343 | 344 | .hoverProfile .boxLink .boxLinkText { 345 | display: flex; 346 | justify-content: flex-end; 347 | align-items: center; 348 | transition: .3s all ease-in-out; 349 | } 350 | 351 | .hoverProfile .boxLink .boxLinkText:hover { 352 | color: #1168df !important; 353 | } 354 | 355 | .hoverProfile .boxLink .boxLinkText:hover .logo { 356 | opacity: 1 !important; 357 | filter: saturate(100%) 358 | } 359 | 360 | .hoverProfile .boxLink .boxLinkText img { 361 | margin-left: 12px; 362 | } 363 | 364 | .hoverProfile .boxLink .profileCheck { 365 | position: absolute; 366 | right: -16px; 367 | } 368 | 369 | .hoverProfile .logo { 370 | opacity: .1 !important; 371 | height: 18px; 372 | width: 18px; 373 | filter: saturate(0%); 374 | transition: .3s all ease-in-out; 375 | } 376 | 377 | .hoverProfile .boxLink a { 378 | color: @lightestFont !important; 379 | font-weight: 300 !important; 380 | } 381 | 382 | .hoverProfile .BoxLinkText { 383 | display: flex; 384 | justify-content: flex-start; 385 | align-items: center; 386 | } 387 | 388 | .hoverProfile .boxLinkEmpty { 389 | font-size: 12px; 390 | text-align: center; 391 | border-radius: 0px 0px 3px 3px; 392 | display: flex; 393 | justify-content: space-between; 394 | align-items: center; 395 | width: 100%; 396 | } 397 | 398 | .hoverProfile .boxLink a { 399 | text-decoration: none; 400 | } 401 | 402 | .hoverProfile .boxLink svg { 403 | color: rgb(72, 125, 236); 404 | } 405 | 406 | .hoverProfile .boxLinkEmpty_text { 407 | display: flex; 408 | justify-content: flex-start; 409 | align-items: center; 410 | } 411 | 412 | .hoverProfile .boxLinkEmpty a { 413 | text-decoration: none; 414 | color: rgb(72, 125, 236); 415 | font-weight: 600; 416 | margin-right: 5px; 417 | } 418 | 419 | .hoverProfile .boxLinkEmpty span { 420 | color: @lightestFont; 421 | } 422 | 423 | .hoverProfile .boxLinkEmpty svg { 424 | color: rgb(72, 125, 236); 425 | } 426 | 427 | .hoverProfile .profileDetails { 428 | margin: 24px 0; 429 | width: 100%; 430 | overflow: hidden; 431 | } 432 | 433 | .hoverProfile .profileValueKey { 434 | font-size: 12px; 435 | width: 20%; 436 | text-align: left; 437 | } 438 | 439 | .hoverProfile:after { 440 | content: ''; 441 | position: absolute; 442 | width: 10px; 443 | height: 10px; 444 | border-bottom: 1px solid rgb(237, 242, 251); 445 | border-right: 1px solid rgb(237, 242, 251); 446 | background: white; 447 | z-index: 0; 448 | } 449 | 450 | .hoverWrap.bottom .hoverProfile:after { 451 | top: -6px; 452 | left: 14%; 453 | margin-left: -10px; 454 | -moz-transform: rotate(225deg); 455 | -webkit-transform: rotate(225deg); 456 | transform: rotate(225deg); 457 | } 458 | 459 | .hoverWrap.top .hoverProfile:after { 460 | bottom: -6px; 461 | left: 14%; 462 | margin-left: -10px; 463 | -moz-transform: rotate(45deg); 464 | -webkit-transform: rotate(45deg); 465 | transform: rotate(45deg); 466 | } 467 | 468 | .hoverWrap.right .hoverProfile:after { 469 | top: 37px; 470 | left: -6px; 471 | margin-top: -10px; 472 | -moz-transform: rotate(135deg); 473 | -webkit-transform: rotate(135deg); 474 | transform: rotate(135deg); 475 | } 476 | 477 | .hoverWrap.left .hoverProfile:after { 478 | top: 37px; 479 | right: -6px; 480 | margin-top: -10px; 481 | -moz-transform: rotate(315deg); 482 | -webkit-transform: rotate(315deg); 483 | transform: rotate(315deg); 484 | } 485 | 486 | .hoverProfile .addressAndCheck { 487 | display: flex; 488 | justify-content: flex-start; 489 | align-items: center; 490 | width: fit-content; 491 | } 492 | 493 | .hoverProfile .addressWrapper { 494 | background-color: rgb(249, 249, 249); 495 | padding: 4px 6px; 496 | border-radius: 3px; 497 | cursor: pointer; 498 | display: flex; 499 | justify-content: flex-start; 500 | align-items: center; 501 | } 502 | 503 | .hoverProfile .addressWrapper img { 504 | width: 13px; 505 | height: 13px; 506 | opacity: .3; 507 | margin-right: 6px; 508 | } 509 | 510 | .hoverProfile .address { 511 | font-size: 12px; 512 | color: @lightestFont; 513 | } 514 | 515 | .loadingText { 516 | text-align: center; 517 | padding: 2px; 518 | font-size: 13px; 519 | } 520 | 521 | .noMargin { 522 | margin: 0 !important; 523 | } 524 | 525 | .onClickOutsideMobile { 526 | display: flex; 527 | position: fixed; 528 | top: 0; 529 | left: 0; 530 | width: 100%; 531 | height: 100%; 532 | z-index: 1; 533 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import makeBlockie from 'ethereum-blockies-base64'; 2 | 3 | import { library, dom as faDom } from "@fortawesome/fontawesome-svg-core" 4 | import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck" 5 | import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight" 6 | import { faGlobeAmericas } from "@fortawesome/free-solid-svg-icons/faGlobeAmericas" 7 | import { faTwitter } from "@fortawesome/free-brands-svg-icons/faTwitter" 8 | import { faGithub } from "@fortawesome/free-brands-svg-icons/faGithub" 9 | import { faClone } from '@fortawesome/free-regular-svg-icons/faClone' 10 | 11 | library.add(faCheck, faArrowRight, faGithub, faTwitter, faGlobeAmericas, faClone); 12 | faDom.watch() 13 | 14 | export const getAddressDisplay = (address, display) => { 15 | const displayShort = display !== 'full' 16 | const addressDisplay = displayShort ? getShortAddress(address) : address 17 | return addressDisplay.toLowerCase(); 18 | } 19 | 20 | const getShortAddress = (address) => { 21 | return address.substr(0,6) + '...' + address.substr(-4); 22 | }; 23 | 24 | const getImgSrc = (profile, address, type) => { 25 | try { 26 | if (!profile.image && type === 'image' && address) return makeBlockie(address); 27 | if (!profile.coverPhoto && type === 'coverPhoto' && address) return null; 28 | 29 | const hash = profile[type] ? profile[type][0].contentUrl["/"] : ''; 30 | return `https://ipfs.infura.io/ipfs/${hash}`; 31 | } catch (error) { 32 | console.log(error); 33 | } 34 | }; 35 | 36 | export const formatUrl = (url) => { 37 | if (!url) { 38 | return undefined; 39 | } 40 | return url.includes('http') ? url : `http://${url}`; 41 | }; 42 | 43 | export const addToClipBoardLinks = (link, copySuccessfulFunc, key) => { 44 | addToClipboard(link); 45 | copySuccessfulFunc(key); 46 | } 47 | 48 | export const addToClipboard = (address) => { 49 | const el = document.createElement('textarea'); 50 | el.readOnly = true; // prevent zoom on iPhone 51 | el.value = address 52 | document.body.appendChild(el); 53 | 54 | 55 | let selection; 56 | let range; 57 | 58 | if (navigator.userAgent.match(/ipad|iphone/i)) { 59 | range = document.createRange(); 60 | range.selectNodeContents(el); 61 | selection = window.getSelection(); 62 | selection.removeAllRanges(); 63 | selection.addRange(range); 64 | el.setSelectionRange(0, 999999); 65 | } else { 66 | el.select(); 67 | } 68 | 69 | document.execCommand('copy'); 70 | document.body.removeChild(el); 71 | }; 72 | 73 | export const formatProfileData = (profile = {}, verified = {}, address, addressDisplay) => { 74 | try { 75 | return { 76 | imgSrc: getImgSrc(profile, address, 'image'), 77 | address: address, 78 | addressDisplay: addressDisplay, 79 | github: verified.github ? verified.github.username : undefined, 80 | twitter: verified.twitter ? verified.twitter.username : undefined, 81 | emoji: profile.emoji, 82 | name: profile.name, 83 | website: profile.website, 84 | description: profile.description, 85 | coverPhoto: getImgSrc(profile, address, 'coverPhoto'), 86 | websiteUrl: formatUrl(profile.website), 87 | }; 88 | } catch (e) { 89 | return { 90 | address, 91 | addressDisplay: addressDisplay.toLowerCase(), 92 | imgSrc: makeBlockie(address) 93 | } 94 | } 95 | }; 96 | 97 | export const checkIsMobile = () => { 98 | try { 99 | if ((typeof window.orientation !== "undefined") 100 | || (navigator.userAgent.indexOf('IEMobile') !== -1)) { 101 | return true; 102 | }; 103 | return false; 104 | } catch (error) { 105 | console.error(error); 106 | } 107 | } -------------------------------------------------------------------------------- /src/widget.js: -------------------------------------------------------------------------------- 1 | import dom, { Fragment } from 'jsx-render'; 2 | import { getProfile, getVerifiedAccounts } from "3box/lib/api"; 3 | import { getAddressDisplay, formatProfileData, addToClipboard } from './utils'; 4 | import store from 'store' 5 | import makeBlockie from 'ethereum-blockies-base64'; 6 | const { BaseTemplate, LoadingTemplate } = require('./html')({ dom, Fragment }); 7 | 8 | import style from './style.less'; 9 | const css = style.toString() 10 | 11 | // local store settings, cache for profile requests 12 | const expirePlugin = require('store/plugins/expire') 13 | store.addPlugin(expirePlugin) 14 | const ttl = 1000 * 60 * 15 15 | const expireAt = () => new Date().getTime() + ttl 16 | 17 | // Plugin 18 | const injectCSS = () => { 19 | const sheet = document.createElement('style') 20 | sheet.type = 'text/css'; 21 | sheet.appendChild(document.createTextNode(css)); 22 | document.body.appendChild(sheet); 23 | } 24 | 25 | const initPlugins = (buttonArray) => { 26 | for (let i = 0; i < buttonArray.length; i++) { 27 | let { address, display, theme } = buttonArray[i].dataset 28 | theme = !(theme === 'none') 29 | const addressDisplay = getAddressDisplay(address, display) 30 | const html = theme ? undefined : buttonArray[i].innerHTML 31 | setProfileContent(buttonArray[i], LoadingTemplate({ 32 | data: {address, addressDisplay, imgSrc: makeBlockie(address)}, 33 | opts: {html} 34 | })) 35 | } 36 | } 37 | 38 | const loadPluginData = async (buttonArray) => { 39 | store.removeExpiredKeys() 40 | for (let i = 0; i < buttonArray.length; i++) { 41 | // get address, maybe do map instead, add other options here after 42 | let { address, display, theme } = buttonArray[i].dataset 43 | theme = !(theme === 'none') 44 | const addressDisplay = getAddressDisplay(address, display) 45 | 46 | const profile = await retrieveProfile(address) 47 | const verified = profile.verified 48 | const data = formatProfileData(profile, verified, address, addressDisplay); 49 | const html = theme ? undefined : buttonArray[i].querySelector("#orginal_html_f1kx").innerHTML 50 | 51 | if (profile.status === 'error') { 52 | setProfileContent(buttonArray[i], BaseTemplate({ data })) 53 | } else { 54 | setProfileContent(buttonArray[i], BaseTemplate({ data, opts: {html} })) 55 | } 56 | } 57 | } 58 | 59 | const retrieveProfile = async (address) => { 60 | const cacheProfile = await store.get(address) 61 | if (profile) { 62 | return JSON.parse(cacheProfile) 63 | } 64 | 65 | const profile = await getProfile(address) 66 | const verified = await getVerifiedAccounts(profile) 67 | const setCacheProfile = Object.assign(profile, { verified }) 68 | store.set(address, JSON.stringify(setCacheProfile), expireAt()) 69 | 70 | return profile 71 | } 72 | 73 | const copyAddress = (target, address) => { 74 | addToClipboard(address) 75 | copyToCheck(target) 76 | setTimeout(() => checkToCopy(target), 2000) 77 | } 78 | 79 | const copyToCheck = (target) => { 80 | target.querySelector('.check').style = 'display: block;' 81 | } 82 | 83 | const checkToCopy = (target) => { 84 | target.querySelector('.check').style = 'display: none;' 85 | } 86 | 87 | const createPlugins = () => { 88 | injectCSS() 89 | 90 | window['boxCopyAddress_f1kx'] = copyAddress 91 | 92 | document.addEventListener("DOMContentLoaded", function(event) { 93 | const buttonArray = document.getElementsByTagName("threebox-address") 94 | initPlugins(buttonArray) 95 | window.addEventListener('load', async () => { 96 | loadPluginData(buttonArray) 97 | }) 98 | }) 99 | } 100 | 101 | const pluginAddedListener = () => { 102 | const target = document.body 103 | const config = { 104 | childList: true, 105 | } 106 | function subscriber(mutations) { 107 | mutations.forEach((mutation) => { 108 | if (mutation.addedNodes.length > 0) { 109 | const newElements = Array.from(mutation.addedNodes) 110 | const addressTags = newElements.filter(el => el.tagName === 'THREEBOX-ADDDRESS') 111 | initPlugins(addressTags) 112 | loadPluginData(addressTags) 113 | } 114 | }) 115 | } 116 | 117 | const observer = new MutationObserver(subscriber); 118 | 119 | observer.observe(target, config); 120 | } 121 | 122 | const setProfileContent = (element, newChild) => { 123 | element.childNodes.forEach(child => child.remove()); 124 | element.appendChild(newChild); 125 | } 126 | 127 | pluginAddedListener() 128 | 129 | createPlugins() 130 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const createConfig = ({ target, entry, output, babelOptions = {}, externals = {} }) => ({ 4 | entry, 5 | output, 6 | externals, 7 | watch: true, 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.(js|jsx)$/, 12 | exclude: /(node_modules)/, 13 | resolve: { 14 | extensions: getExtensions(target) 15 | }, 16 | use: [ 17 | { 18 | loader: "babel-loader", 19 | options: { 20 | babelrc: false, 21 | ...babelOptions 22 | } 23 | } 24 | ] 25 | }, 26 | { 27 | test: /\.less$/, 28 | use: [ 29 | { 30 | loader: "css-loader", 31 | options: { 32 | sourceMap: true, 33 | modules: true, 34 | localIdentName: "threeboxProfileHover__[name]_[local]" 35 | } 36 | }, 37 | { 38 | loader: "less-loader" 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | }); 45 | 46 | const getExtensions = (target) => { 47 | const extensions = [".js", ".jsx"]; 48 | 49 | if (!target) { 50 | return extensions; 51 | } 52 | 53 | return extensions 54 | .map(extension => `_${target}${extension}`) 55 | .concat(extensions); 56 | } 57 | 58 | module.exports = (env, argv) => { 59 | const production = argv.mode === 'production'; 60 | 61 | const nonReactConfig = createConfig({ 62 | entry: "./src/widget.js", 63 | output: { 64 | filename: "widget.js", 65 | path: path.resolve(__dirname, "dist") 66 | }, 67 | babelOptions: { 68 | presets: ["@babel/preset-env"], 69 | plugins: [ 70 | ["@babel/plugin-transform-react-jsx", { pragma: "dom" }], 71 | ["@babel/plugin-transform-runtime", { regenerator: true }], 72 | ["@babel/plugin-proposal-object-rest-spread"] 73 | ] 74 | } 75 | }); 76 | 77 | const reactConfig = createConfig({ 78 | target: "react", 79 | entry: "./src/ProfileHover.jsx", 80 | output: { 81 | libraryTarget: "commonjs2", 82 | filename: "reactBundle.js", 83 | path: path.resolve(__dirname, "dist") 84 | }, 85 | babelOptions: { 86 | presets: ["@babel/preset-env", "@babel/preset-react"], 87 | plugins: [ 88 | ["@babel/plugin-transform-react-jsx"], 89 | ["@babel/plugin-transform-runtime", { regenerator: true }] 90 | ] 91 | }, 92 | externals: !production ? undefined : { 93 | react: "react" 94 | } 95 | }); 96 | 97 | return [nonReactConfig, reactConfig]; 98 | } 99 | 100 | -------------------------------------------------------------------------------- /webpack.example.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: "./example/react/app.js", 5 | output: { 6 | filename: "bundle.js", 7 | path: path.resolve(__dirname, "example", "react") 8 | }, 9 | watch: true, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx)$/, 14 | exclude: /(node_modules)/, 15 | resolve: { 16 | extensions: [".js", ".jsx"] 17 | }, 18 | use: [ 19 | { 20 | loader: "babel-loader", 21 | options: { 22 | babelrc: false, 23 | presets: ["@babel/preset-env", "@babel/preset-react"], 24 | plugins: [ 25 | ["@babel/plugin-transform-modules-commonjs"], 26 | ["@babel/plugin-transform-react-jsx"], 27 | ["@babel/plugin-transform-runtime", { regenerator: true }] 28 | ] 29 | } 30 | } 31 | ] 32 | }, 33 | { 34 | test: /\.less$/, 35 | use: [ 36 | { 37 | loader: "css-loader", 38 | options: { 39 | sourceMap: true, 40 | modules: true, 41 | localIdentName: "threeboxProfileHover__[name]_[local]" 42 | } 43 | }, 44 | { 45 | loader: "less-loader" 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | }; 52 | 53 | --------------------------------------------------------------------------------