├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── main.yml │ └── release-package.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .storybook ├── main.js └── preview.js ├── .vscode └── react-reactions.code-workspace ├── LICENSE ├── README.md ├── assets └── react-reactions-media.png ├── example ├── .gitignore ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public │ └── fonts │ │ ├── slack-icons-Regular.eot │ │ ├── slack-icons-Regular.ttf │ │ └── slack-icons-Regular.woff ├── src │ ├── App.css │ ├── App.tsx │ ├── favicon.svg │ ├── index.css │ ├── logo.svg │ ├── main.tsx │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── renovate.json ├── src ├── components │ ├── custom │ │ ├── ReactionBarSelector.tsx │ │ ├── ReactionBarSelectorEmoji.tsx │ │ ├── ReactionCounter.tsx │ │ ├── ReactionCounterEmoji.tsx │ │ └── index.ts │ ├── facebook │ │ ├── FacebookCounter.tsx │ │ ├── FacebookCounterReaction.tsx │ │ ├── FacebookSelector.tsx │ │ ├── FacebookSelectorEmoji.tsx │ │ └── index.ts │ ├── github │ │ ├── GithubCounter.tsx │ │ ├── GithubCounterGroup.tsx │ │ ├── GithubSelector.tsx │ │ ├── GithubSelectorEmoji.tsx │ │ └── index.ts │ ├── index.ts │ ├── pokemon │ │ ├── PokemonCounter.tsx │ │ ├── PokemonSelector.tsx │ │ └── index.ts │ ├── slack │ │ ├── SlackCSS.tsx │ │ ├── SlackCounter.tsx │ │ ├── SlackCounterGroup.tsx │ │ ├── SlackSelector.tsx │ │ ├── SlackSelectorFooter.tsx │ │ ├── SlackSelectorHeader.tsx │ │ ├── SlackSelectorHeaderTab.tsx │ │ ├── SlackSelectorItems.tsx │ │ ├── SlackSelectorSection.tsx │ │ ├── SlackSelectorSectionEmoji.tsx │ │ ├── fonts │ │ │ ├── slack-icons-Regular.eot │ │ │ ├── slack-icons-Regular.ttf │ │ │ └── slack-icons-Regular.woff │ │ └── index.ts │ └── youtube │ │ ├── YoutubeCounter.tsx │ │ ├── YoutubeCounterButton.tsx │ │ └── index.ts ├── helpers │ ├── Hover.tsx │ ├── HoverStyle.tsx │ ├── emoji.ts │ ├── groupBy.ts │ ├── icons.ts │ ├── index.ts │ ├── interfaces.ts │ ├── slack.ts │ ├── strings.ts │ ├── types.ts │ ├── useHover.ts │ └── withActive.tsx └── index.ts ├── stories ├── FacebookCounter.stories.tsx ├── FacebookCounterReaction.stories.tsx ├── FacebookSelector.stories.tsx ├── FacebookSelectorEmoji.stories.tsx ├── GithubCounter.stories.tsx ├── GithubCounterGroup.stories.tsx ├── GithubSelector.stories.tsx ├── GithubSelectorEmoji.stories.tsx ├── PokemonCounter.stories.tsx ├── PokemonSelector.css ├── PokemonSelector.stories.tsx ├── ReactionBarSelector.stories.tsx ├── ReactionBarSelectorEmoji.stories.tsx ├── ReactionCounter.stories.tsx ├── ReactionCounterBackground.stories.tsx ├── ReactionCounterEmoji.stories.tsx ├── SlackCounter.stories.tsx ├── SlackCounterGroup.stories.tsx ├── SlackExample.stories.tsx ├── SlackSelector.stories.tsx ├── SlackSelectorFooter.stories.tsx ├── SlackSelectorHeader.stories.tsx ├── SlackSelectorHeaderTab.stories.tsx ├── SlackSelectorItems.stories.tsx ├── SlackSelectorSection.stories.tsx ├── SlackSelectorSectionEmoji.stories.tsx ├── YoutubeCounter.stories.tsx ├── YoutubeCounterButton.stories.tsx ├── active.stories.tsx └── helper.css ├── tsconfig.json └── tsdx.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["react-hooks"], 3 | "rules": { 4 | "react-hooks/rules-of-hooks": "error", 5 | "react-hooks/exhaustive-deps": "warn", 6 | "prettier/prettier": [ 7 | "error", 8 | { "endOfLine": "auto" }, 9 | { "usePrettierrc": true } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [charkour] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['12.x', '14.x', '16.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - uses: pnpm/action-setup@v2.1.0 18 | with: 19 | version: 6.32.8 20 | - name: Use Node ${{ matrix.node }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node }} 24 | cache: 'pnpm' 25 | 26 | - run: pnpm i 27 | 28 | - name: Lint 29 | run: yarn lint 30 | 31 | - name: Test 32 | run: yarn test --ci --coverage --maxWorkers=2 33 | 34 | - name: Build 35 | run: yarn build 36 | -------------------------------------------------------------------------------- /.github/workflows/release-package.yml: -------------------------------------------------------------------------------- 1 | name: Publish Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: pnpm/action-setup@v2.1.0 13 | with: 14 | version: 6.32.8 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 16 18 | cache: 'pnpm' 19 | - run: pnpm i 20 | - run: pnpm test 21 | 22 | publish-gpr: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: pnpm/action-setup@v2.1.0 28 | with: 29 | version: 6.32.8 30 | - uses: actions/setup-node@v2 31 | with: 32 | node-version: 16 33 | registry-url: https://npm.pkg.github.com/ 34 | cache: 'pnpm' 35 | - run: pnpm i 36 | - run: pnpm publish 37 | env: 38 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 39 | # Use to publish to npm 40 | # publish-2: 41 | # needs: build 42 | # runs-on: ubuntu-latest 43 | # steps: 44 | # - uses: actions/checkout@v2 45 | # - uses: actions/setup-node@v2 46 | # with: 47 | # node-version: 12 48 | # registry-url: https://registry.npmjs.org/ 49 | # - run: npm ci 50 | # - run: npm publish --access public 51 | # env: 52 | # NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run fix -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: [ 3 | '../stories/**/*.stories.mdx', 4 | '../stories/**/*.stories.@(js|jsx|ts|tsx)', 5 | ], 6 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'], 7 | // https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration 8 | typescript: { 9 | check: true, // type-check stories during Storybook build 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // https://storybook.js.org/docs/react/writing-stories/parameters#global-parameters 2 | export const parameters = { 3 | // https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args 4 | actions: { argTypesRegex: '^on.*' }, 5 | }; 6 | -------------------------------------------------------------------------------- /.vscode/react-reactions.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "../" 5 | } 6 | ], 7 | "settings": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode", 9 | "editor.formatOnSave": true, 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": true, 12 | "source.organizeImports": true, 13 | "source.fixAll": true 14 | }, 15 | "eslint.workingDirectories": [ 16 | { 17 | "mode": "auto" 18 | } 19 | ], 20 | "[ignore]": { 21 | "editor.defaultFormatter": "stylelint.vscode-stylelint" 22 | } 23 | }, 24 | "extensions": { 25 | "recommendations": [ 26 | "esbenp.prettier-vscode", 27 | "dbaeumer.vscode-eslint", 28 | "DavidAnson.vscode-markdownlint", 29 | "streetsidesoftware.code-spell-checker" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Charles Kornoelje 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 | # ![react-reactions](./assets/react-reactions-media.png) 2 | 3 | Create your own reaction bar or use one of your favorites! 4 | 5 | - **4 Different Selectors** - Slack, Facebook, Pokemon and GitHub 6 | - **5 Different Counters** - GitHub, YouTube, Facebook, Pokemon, and Slack 7 | 8 | [![Downloads](https://img.shields.io/npm/dt/@charkour/react-reactions.svg)](https://www.npmjs.com/package/@charkour/react-reactions) 9 | 10 | Install [via npm](https://www.npmjs.com/package/@charkour/react-reactions) (or from the [GitHub Package Registry](https://github.com/charkour/react-reactions/packages/623096)): 11 | 12 | ```sh 13 | npm i @charkour/react-reactions 14 | ``` 15 | 16 | > This originated as a fork of [casesandberg/react-reactions](https://github.com/casesandberg/react-reactions) which is been modified under the MIT license to include additional features. 17 | 18 | ## New Features 19 | 20 | - [x] Add ability to pass **custom icons** 21 | - [x] Fixed security vulnerabilities 22 | - [x] CJS and ESM support 23 | - [x] Add [ref forwarding](https://reactjs.org/docs/forwarding-refs.html) support 24 | - [x] Zero dependencies 25 | - [x] Built in Typescript and modern React (with [TSDX](https://github.com/formium/tsdx)) 26 | - [x] Works with React 16.8+ and Next 10 27 | 28 | ## Road Map 29 | 30 | - [ ] Update current Selectors and Counter to match 2021 styles 31 | - [ ] Add Discord Selector and Counter 32 | - [ ] Add unit testing 33 | - [ ] More??? Suggest a feature on [Github Issues](https://github.com/charkour/react-reactions/issues) 34 | 35 | ## Custom Selectors 36 | 37 | ### Reaction Bar Selector 38 | 39 | ```tsx 40 | import React from 'react'; 41 | import { ReactionBarSelector } from '@charkour/react-reactions'; 42 | 43 | const Component = () => { 44 | return ; 45 | }; 46 | ``` 47 | 48 | **Props:** 49 | 50 | `iconSize?: number` — String icon pixel size. Defaults to `38px` 51 | 52 | `reactions?: Reaction[];` — Array of Reaction objects `{label: "haha", node:
😄
, key: "smile"}` to display. 53 | 54 | `onSelect: (key: string) => void;` — Function callback when emoji is selected 55 | 56 | `style?: React.CSSProperties` - Pass a style object to the selector container 57 | 58 | ![image](https://user-images.githubusercontent.com/33156025/111041592-e1ece100-8406-11eb-82f5-226b3839683c.png) 59 | 60 | Also works with images. 61 | 62 | ![image](https://user-images.githubusercontent.com/33156025/111041788-00071100-8408-11eb-82a3-e23723e0755c.png) 63 | 64 | _Note_: When passing an `` as a Reaction. Specify the `iconSize` as the height of the image. `` 65 | 66 | ### Reaction Counter 67 | 68 | ```tsx 69 | import React from 'react'; 70 | import { ReactionCounter } from '@charkour/react-reactions'; 71 | 72 | const Component = () => { 73 | return ; 74 | }; 75 | ``` 76 | 77 | **Props:*** 78 | 79 | `iconSize?: number` - String icon pixel size. Defaults to `24px` 80 | 81 | `bg?: string` - String of hex color for outline of overlapping reactions. Defaults to `#fff` 82 | 83 | `reactions: ReactionCounterObject[]` - Array of emoji to dispay 84 | 85 | `user?: string` - String name of user so that user displays as `You` 86 | 87 | `important?: string[]` - Array of strings for important users to display their name 88 | 89 | `showReactsOnly?: boolean` - If `true`, only show the Reactions and no text. Defaults to `false` 90 | 91 | `showTotalOnly?: boolean` - If `true`, only show the number of Reactions and no specific names. Defaults to `false` 92 | 93 | `showOthersAlways?: boolean` - Will display "and 0 others" if you are the only person who reacted. Defaults to `true` 94 | 95 | `className?: string` - Pass a string that applies to the counter container 96 | 97 | `onClick?: () => void` - Pass a callback that is added to the `onClick` property to the counter container 98 | 99 | `style?: React.CSSProperties` - Pass a style object to the counter container 100 | 101 | ![image](https://user-images.githubusercontent.com/33156025/135777827-803fac2d-d2c9-4734-8073-bd6e3a6d2160.png) 102 | 103 | ## Selectors 104 | 105 | ### Slack Selector 106 | 107 | ```tsx 108 | import React from 'react'; 109 | import { SlackSelector } from '@charkour/react-reactions'; 110 | 111 | const Component = () => { 112 | return ; 113 | }; 114 | ``` 115 | 116 | **Props:** 117 | 118 | `active`: String of active tab. Defaults to `mine` 119 | 120 | `scrollHeight`: String pixel height of scroll container. Defaults to `270px` 121 | 122 | `removeEmojis`: Array of emojis to remove from emoji list 123 | 124 | `frequent`: Array of emojis to set Frequently Used. Defaults to `['👍', '🐉', '🙌', '🗿', '😊', '🐬', '😹', '👻', '🚀', '🚁', '🏇', '🇨🇦']` 125 | 126 | `onSelect`: Function callback when emoji is selected 127 | 128 | **Fonts**: To use the Slack fonts, download the font files [here](https://github.com/charkour/react-reactions/tree/main/example/public/fonts) and include them in `public/fonts`. 129 | 130 | --- 131 | 132 | ### Github Selector 133 | 134 | ```tsx 135 | import React from 'react'; 136 | import { GithubSelector } from '@charkour/react-reactions'; 137 | 138 | const Component = () => { 139 | return ; 140 | }; 141 | ``` 142 | 143 | **Props:** 144 | 145 | `reactions`: Array of emoji to dispay. Defaults to `['👍', '👎', '😄', '🎉', '😕', '❤️']` 146 | 147 | `onSelect`: Function callback when emoji is selected 148 | 149 | --- 150 | 151 | ### Facebook Selector 152 | 153 | ```tsx 154 | import React from 'react'; 155 | import { FacebookSelector } from '@charkour/react-reactions'; 156 | 157 | const Component = () => { 158 | return ; 159 | }; 160 | ``` 161 | 162 | **Props:** 163 | 164 | `reactions`: Array of strings for reactions to display. Defaults to `['like', 'love', 'haha', 'wow', 'sad', 'angry']` 165 | 166 | `iconSize`: String icon pixel size. Defaults to `38px` 167 | 168 | `onSelect`: Function callback when emoji is selected 169 | 170 | --- 171 | 172 | ### Pokemon Selector 173 | 174 | ```tsx 175 | import React from 'react'; 176 | import { PokemonSelector } from '@charkour/react-reactions'; 177 | 178 | const Component = () => { 179 | return ; 180 | }; 181 | ``` 182 | 183 | **Props:** 184 | 185 | `reactions`: Array of strings for reactions to display. Defaults to `['like', 'love', 'haha', 'wow', 'sad', 'angry']` 186 | 187 | `iconSize`: String icon pixel size. Defaults to `38px` 188 | 189 | `onSelect`: Function callback when emoji is selected 190 | 191 | --- 192 | 193 | ## Counters 194 | 195 | ### Github Counter 196 | 197 | ```tsx 198 | import React from 'react'; 199 | import { GithubCounter } from '@charkour/react-reactions'; 200 | 201 | const Component = () => { 202 | return ; 203 | }; 204 | ``` 205 | 206 | **Props:** 207 | 208 | `counters`: Array of counter objects structured such that: 209 | 210 | ```tsx 211 | { 212 | emoji: '👍', // String emoji reaction 213 | by: 'case', // String of persons name 214 | } 215 | ``` 216 | 217 | `user`: String name of user so that user displays as `You` 218 | 219 | `onSelect`: Function callback when emoji is selected 220 | 221 | `onAdd`: Function callback when add reaction is clicked 222 | 223 | --- 224 | 225 | ### Youtube Counter 226 | 227 | ```tsx 228 | import React from 'react'; 229 | import { YoutubeCounter } from '@charkour/react-reactions'; 230 | 231 | const Component = () => { 232 | return ; 233 | }; 234 | ``` 235 | 236 | **Props:** 237 | 238 | `like`: String number of likes 239 | 240 | `dislike`: String number of dislikes 241 | 242 | `onLikeClick`: Function callback when like is clicked 243 | 244 | `onDislikeClick`: Function callback when dislike is clicked 245 | 246 | --- 247 | 248 | ### Facebook Counter 249 | 250 | ```tsx 251 | import React from 'react'; 252 | import { FacebookCounter } from '@charkour/react-reactions'; 253 | 254 | const Component = () => { 255 | return ; 256 | }; 257 | ``` 258 | 259 | **Props:** 260 | 261 | `counters`: Array of counter objects structured such that: 262 | 263 | ```tsx 264 | { 265 | emoji: 'like', // String name of reaction 266 | by: 'Case Sandberg', // String of persons name 267 | } 268 | ``` 269 | 270 | `user`: String name of user so that user displays as `You` 271 | 272 | `important`: Array of strings for important users to display their name 273 | 274 | `bg`: String of hex color for outline of overlapping reactions. Defaults to `#fff` 275 | 276 | `onClick`: Function callback when clicked 277 | 278 | `alwaysShowOthers`: boolean. Will display "and 0 others" if you are the only person who reacted. 279 | 280 | --- 281 | 282 | ### Pokemon Counter 283 | 284 | ```tsx 285 | import React from 'react'; 286 | import { PokemonCounter } from '@charkour/react-reactions'; 287 | 288 | const Component = () => { 289 | return ; 290 | }; 291 | ``` 292 | 293 | **Props:** 294 | 295 | `counters`: Array of counter objects structured such that: 296 | 297 | ```tsx 298 | { 299 | emoji: 'like', // String name of reaction 300 | by: 'Charles Kornoelje', // String of persons name 301 | } 302 | ``` 303 | 304 | `user`: String name of user so that user displays as `You` 305 | 306 | `important`: Array of strings for important users to display their name 307 | 308 | `bg`: String of hex color for outline of overlapping reactions. Defaults to `#fff` 309 | 310 | `onClick`: Function callback when clicked 311 | 312 | `alwaysShowOthers`: boolean. Will display "and 0 others" if you are the only person who reacted. 313 | 314 | --- 315 | 316 | ### Slack Counter 317 | 318 | ```tsx 319 | import React from 'react'; 320 | import { SlackCounter } from '@charkour/react-reactions'; 321 | 322 | const Component = () => { 323 | return ; 324 | }; 325 | ``` 326 | 327 | **Props:** 328 | 329 | `counters`: Array of counter objects structured such that: 330 | 331 | ```tsx 332 | { 333 | emoji: '🗿', // String emoji reaction 334 | by: 'case', // String of persons name 335 | } 336 | ``` 337 | 338 | `user`: String name of user so that user displays as `You` 339 | 340 | `onSelect`: Function callback when emoji is selected 341 | 342 | `onAdd`: Function callback when add reaction is clicked 343 | 344 | **Fonts**: To use the Slack fonts, download the font files [here](https://github.com/charkour/react-reactions/tree/main/example/public/fonts) and include them in `public/fonts`. 345 | 346 | --- 347 | 348 | ## Animations 349 | 350 | A simple animation can be done on the components using CSS. See this [demo](https://codesandbox.io/s/sweet-burnell-oh5vg?file=/src/App.js). 351 | More advaned animations can be done using dynamic styles. See this [demo](https://codesandbox.io/s/competent-curran-cn4tv?file=/src/App.js) 352 | 353 | ## Development 354 | 355 | ```bash 356 | npm start 357 | ``` 358 | 359 | This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. 360 | 361 | Then run either Storybook or the example playground: 362 | 363 | ### Storybook 364 | 365 | Run inside another terminal: 366 | 367 | ```bash 368 | npm run storybook 369 | ``` 370 | 371 | This loads the stories from `./stories`. 372 | 373 | > NOTE: Stories should reference the components as if using the library, similar to the example playground. This means importing from the root project directory. This has been aliased in the tsconfig and the storybook webpack config as a helper. 374 | 375 | ### Example 376 | 377 | Then run the example inside another: 378 | 379 | ```bash 380 | cd example 381 | npm i 382 | npm start 383 | ``` 384 | 385 | The default example imports and live reloads whatever is in `/dist`, so if you are seeing an out of date component, make sure TSDX is running in watch mode like we recommend above. **No symlinking required**, we use [Parcel's aliasing](https://parceljs.org/module_resolution.html#aliases). 386 | 387 | To do a one-off build, use `npm run build`. 388 | 389 | To run tests, use `npm test`. 390 | 391 | ## Configuration 392 | 393 | Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. 394 | 395 | ### Jest 396 | 397 | Jest tests are set up to run with `npm test`. 398 | 399 | ### GitHub Actions 400 | 401 | - `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix 402 | 403 | --- 404 | 405 | > Pokemon Illustrations by [Chris Owens](https://dribbble.com/monkee1895) 406 | -------------------------------------------------------------------------------- /assets/react-reactions-media.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charkour/react-reactions/bf2b92846ec49d882678e0eb869190176e71d086/assets/react-reactions-media.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Reactions 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-reactions-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@charkour/react-reactions": "0.6.3", 12 | "react": "17.0.2", 13 | "react-dom": "17.0.2" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "17.0.39", 17 | "@types/react-dom": "17.0.13", 18 | "@vitejs/plugin-react": "1.2.0", 19 | "autoprefixer": "10.4.2", 20 | "postcss": "8.4.8", 21 | "tailwindcss": "3.0.23", 22 | "typescript": "4.6.2", 23 | "vite": "2.8.6" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /example/public/fonts/slack-icons-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charkour/react-reactions/bf2b92846ec49d882678e0eb869190176e71d086/example/public/fonts/slack-icons-Regular.eot -------------------------------------------------------------------------------- /example/public/fonts/slack-icons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charkour/react-reactions/bf2b92846ec49d882678e0eb869190176e71d086/example/public/fonts/slack-icons-Regular.ttf -------------------------------------------------------------------------------- /example/public/fonts/slack-icons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charkour/react-reactions/bf2b92846ec49d882678e0eb869190176e71d086/example/public/fonts/slack-icons-Regular.woff -------------------------------------------------------------------------------- /example/src/App.css: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: 15vw; 3 | letter-spacing: -1.1vw; 4 | font-weight: 800; 5 | color: white; 6 | white-space: wrap; 7 | line-height: 90%; 8 | } 9 | 10 | .header-background-blue { 11 | background: rgba(3, 0, 179); 12 | } 13 | 14 | html { 15 | @apply header-background-blue; 16 | } 17 | 18 | .github { 19 | /* padding: 15px; 20 | width: 24px; 21 | position: fixed; 22 | top: 0px; 23 | right: 0px; 24 | fill: rgba(0, 0, 0, 0.3); 25 | cursor: pointer; */ 26 | } 27 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FacebookCounter, 3 | FacebookSelector, 4 | GithubCounter, 5 | GithubSelector, 6 | PokemonCounter, 7 | PokemonSelector, 8 | SlackCounter, 9 | SlackSelector, 10 | YoutubeCounter, 11 | } from '@charkour/react-reactions'; 12 | import React from 'react'; 13 | import './App.css'; 14 | 15 | async function getFileFromUrl( 16 | url: string, 17 | name: string, 18 | defaultType = 'image/jpeg' 19 | ) { 20 | const response = await fetch(url); 21 | const data = await response.blob(); 22 | return new File([data], name, { 23 | type: response.headers.get('content-type') || defaultType, 24 | }); 25 | } 26 | 27 | const read = new FileReader(); 28 | 29 | const App: React.FC = () => { 30 | const [markdown, setMarkdown] = React.useState(); 31 | 32 | React.useEffect(() => { 33 | (async () => { 34 | const file = await getFileFromUrl( 35 | 'https://raw.githubusercontent.com/charkour/react-reactions/main/README.md', 36 | 'readme.md' 37 | ); 38 | read.readAsBinaryString(file); 39 | read.onloadend = function () { 40 | setMarkdown(read.result as string); 41 | console.log(read.result); 42 | }; 43 | })(); 44 | }, []); 45 | 46 | return ( 47 | <> 48 |
49 |
50 | 😲 51 | 56 | 57 | 58 | 59 | 60 |
61 |
62 |
63 | React Reactions 64 |
65 |
66 |
67 | 74 |
75 |
selectors
76 | 77 | 78 | 79 | 80 |
counters
81 | 82 | 83 | 84 | 85 | 86 |
87 | 88 | ); 89 | }; 90 | 91 | export default App; 92 | -------------------------------------------------------------------------------- /example/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | html { 3 | font-family: 'Inter', sans-serif; 4 | } 5 | @supports (font-variation-settings: normal) { 6 | html { 7 | font-family: 'Inter var', sans-serif; 8 | } 9 | } 10 | 11 | body { 12 | margin: 0; 13 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 14 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 15 | sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | 20 | #root, 21 | html, 22 | body { 23 | width: 100%; 24 | height: 100%; 25 | overflow-x: hidden; 26 | } 27 | 28 | @tailwind base; 29 | @tailwind components; 30 | @tailwind utilities; 31 | -------------------------------------------------------------------------------- /example/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ) 12 | -------------------------------------------------------------------------------- /example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@charkour/react-reactions", 3 | "version": "0.11.0", 4 | "private": false, 5 | "description": "😲 Create custom reaction pickers and counters or use your favorites!", 6 | "keywords": [ 7 | "react", 8 | "reactions", 9 | "custom", 10 | "reaction picker", 11 | "react-component", 12 | "reaction-picker", 13 | "slack", 14 | "pokemon", 15 | "github", 16 | "facebook", 17 | "youtube", 18 | "selector", 19 | "counter" 20 | ], 21 | "homepage": "https://github.com/charkour/react-reactions", 22 | "bugs": { 23 | "url": "https://github.com/charkour/react-reactions/issues" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/charkour/react-reactions.git" 28 | }, 29 | "license": "MIT", 30 | "author": "Charles Kornoelje", 31 | "main": "dist/index.js", 32 | "module": "dist/react-reactions.esm.js", 33 | "typings": "dist/index.d.ts", 34 | "files": [ 35 | "dist", 36 | "src" 37 | ], 38 | "scripts": { 39 | "build": "tsdx build", 40 | "build-storybook": "build-storybook", 41 | "fix": "tsdx lint src --fix", 42 | "postinstall": "husky install", 43 | "lint": "tsdx lint src", 44 | "prepare": "tsdx build", 45 | "prepublishOnly": "pinst --disable", 46 | "postpublish": "pinst --enable", 47 | "storybook": "start-storybook -p 6006", 48 | "test": "tsdx test --passWithNoTests" 49 | }, 50 | "prettier": { 51 | "printWidth": 80, 52 | "semi": true, 53 | "singleQuote": true, 54 | "trailingComma": "es5" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "7.18.6", 58 | "@storybook/addon-actions": "6.5.9", 59 | "@storybook/addon-essentials": "6.5.9", 60 | "@storybook/addon-links": "6.5.9", 61 | "@storybook/react": "6.5.9", 62 | "@types/jest": "^28.1.4", 63 | "@types/react": "18.0.14", 64 | "@types/react-dom": "18.0.5", 65 | "autoprefixer": "10.4.7", 66 | "cssnano": "5.1.12", 67 | "eslint": "8.19.0", 68 | "eslint-plugin-react-hooks": "4.6.0", 69 | "husky": "7.0.4", 70 | "pinst": "3.0.0", 71 | "postcss": "8.4.14", 72 | "postcss-import": "14.1.0", 73 | "react": "18.2.0", 74 | "react-dom": "18.2.0", 75 | "require-from-string": "2.0.2", 76 | "rollup-plugin-postcss": "4.0.2", 77 | "tsdx": "0.14.1", 78 | "tslib": "2.4.0", 79 | "typescript": "4.7.4", 80 | "webpack": "5.73.0" 81 | }, 82 | "peerDependencies": { 83 | "react": ">=16" 84 | }, 85 | "engines": { 86 | "node": ">=10" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/components/custom/ReactionBarSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Reaction } from '../../helpers'; 3 | import ReactionBarSelectorEmoji from './ReactionBarSelectorEmoji'; 4 | 5 | export interface ReactionBarSelectorProps { 6 | iconSize?: number; 7 | reactions?: Reaction[]; 8 | onSelect?: (key: string) => void; 9 | style?: React.CSSProperties; 10 | } 11 | 12 | export const ReactionBarSelector = React.forwardRef< 13 | HTMLDivElement, 14 | ReactionBarSelectorProps 15 | >( 16 | ( 17 | { 18 | iconSize = defaultProps.iconSize, 19 | reactions = defaultProps.reactions, 20 | onSelect = defaultProps.onSelect, 21 | style = defaultProps.style, 22 | }, 23 | ref 24 | ) => { 25 | const emojiStyle = React.useMemo(() => { 26 | return { 27 | width: `${iconSize + 10}px`, 28 | height: `${iconSize + 10}px`, 29 | display: 'flex', 30 | alignItems: 'center', 31 | fontSize: iconSize, 32 | }; 33 | }, [iconSize]); 34 | 35 | return ( 36 |
37 | {reactions.map((reaction: Reaction) => { 38 | return ( 39 |
40 | 44 |
45 | ); 46 | })} 47 |
48 | ); 49 | } 50 | ); 51 | 52 | export const defaultProps: Required = { 53 | style: {}, 54 | reactions: [ 55 | { node:
👍
, label: 'like', key: 'satisfaction' }, 56 | { node:
❤️
, label: 'love', key: 'love' }, 57 | { node:
😆
, label: 'haha', key: 'happy' }, 58 | { node:
😮
, label: 'wow', key: 'surprise' }, 59 | { node:
😢
, label: 'sad', key: 'sad' }, 60 | { node:
😡
, label: 'angry', key: 'angry' }, 61 | ], 62 | iconSize: 38, 63 | onSelect: (key: string) => { 64 | console.log(key); 65 | }, 66 | }; 67 | 68 | const wrapStyle = { 69 | backgroundColor: '#fff', 70 | borderRadius: '50px', 71 | padding: '2px', 72 | boxShadow: '0 0 0 1px rgba(0, 0, 0, .05), 0 1px 2px rgba(0, 0, 0, .15)', 73 | display: 'flex', 74 | width: 'fit-content', 75 | }; 76 | 77 | export default ReactionBarSelector; 78 | -------------------------------------------------------------------------------- /src/components/custom/ReactionBarSelectorEmoji.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Hover, HoverStyle, Reaction } from '../../helpers'; 3 | 4 | export interface ReactionBarSelectorEmojiProps { 5 | reaction: Reaction; 6 | onSelect: (label: string) => void; 7 | } 8 | 9 | export const ReactionBarSelectorEmoji = React.forwardRef< 10 | HTMLDivElement, 11 | ReactionBarSelectorEmojiProps 12 | >(({ reaction, onSelect }, ref) => { 13 | const { node, label, key } = reaction; 14 | 15 | const handleClick = () => { 16 | onSelect && onSelect(key ?? label); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | {label} 23 | 24 | 29 | {node} 30 | 31 | 32 | ); 33 | }); 34 | 35 | const wrapStyle: React.CSSProperties = { 36 | padding: '5px', 37 | position: 'relative', 38 | }; 39 | const labelStyleHover = { 40 | transform: 'translateX(-50%) translateY(-10px)', 41 | opacity: '1', 42 | }; 43 | const labelStyle: React.CSSProperties = { 44 | position: 'absolute', 45 | top: '-22px', 46 | background: 'rgba(0,0,0,.8)', 47 | borderRadius: '14px', 48 | color: '#fff', 49 | fontSize: '11px', 50 | padding: '4px 7px 3px', 51 | fontWeight: 'bold', 52 | textTransform: 'capitalize', 53 | left: '50%', 54 | transform: 'translateX(-50%)', 55 | transition: '200ms transform cubic-bezier(0.23, 1, 0.32, 1)', 56 | opacity: '0', 57 | fontFamily: 'sans-serif', 58 | }; 59 | const iconStyle = { 60 | transformOrigin: 'bottom', 61 | cursor: 'pointer', 62 | transition: '200ms transform cubic-bezier(0.23, 1, 0.32, 1)', 63 | }; 64 | const iconStyleHover = { transform: 'scale(1.3)' }; 65 | 66 | export default ReactionBarSelectorEmoji; 67 | -------------------------------------------------------------------------------- /src/components/custom/ReactionCounter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { groupBy, listOfNames, ReactionCounterObject } from '../../helpers'; 3 | import { 4 | ReactionCounterEmoji, 5 | ReactionCounterEmojiProps, 6 | } from './ReactionCounterEmoji'; 7 | 8 | export interface ReactionCounterProps 9 | extends Partial> { 10 | reactions: ReactionCounterObject[]; 11 | user?: string; 12 | important?: string[]; 13 | showReactsOnly?: boolean; 14 | showTotalOnly?: boolean; 15 | showOthersAlways?: boolean; 16 | className?: string; 17 | onClick?: () => void; 18 | style?: React.CSSProperties; 19 | } 20 | 21 | export const ReactionCounter = React.forwardRef< 22 | HTMLDivElement, 23 | ReactionCounterProps 24 | >( 25 | ( 26 | { 27 | reactions, 28 | user, 29 | important, 30 | className, 31 | onClick, 32 | iconSize = 24, 33 | bg = '#FFF', 34 | showReactsOnly = false, 35 | showTotalOnly = false, 36 | showOthersAlways = true, 37 | style, 38 | }, 39 | ref 40 | ) => { 41 | const groups = groupBy(reactions, 'label'); 42 | const names = reactions.map(({ by }: ReactionCounterObject) => { 43 | return by; 44 | }); 45 | 46 | const nameString = []; 47 | if (user && names.includes(user)) { 48 | nameString.push('You'); 49 | } 50 | if (important?.length) { 51 | if (names.includes(important[0])) { 52 | nameString.push(important[0]); 53 | } 54 | if (names.includes(important[1])) { 55 | nameString.push(important[1]); 56 | } 57 | } 58 | const othersCount = names.length - nameString.length; 59 | if (showOthersAlways || othersCount > 0) { 60 | nameString.push(`${othersCount} other${othersCount === 1 ? '' : 's'}`); 61 | } 62 | 63 | const nameStyle: React.CSSProperties = React.useMemo(() => { 64 | return { 65 | fontSize: `${iconSize - 8}px`, 66 | marginLeft: '8px', 67 | }; 68 | }, [iconSize]); 69 | 70 | return ( 71 |
77 | {Object.keys(groups).map((reaction, i, reactions) => ( 78 | 85 | ))} 86 | {!showReactsOnly ? ( 87 |
88 | {showTotalOnly ? names.length : listOfNames(nameString)} 89 |
90 | ) : null} 91 |
92 | ); 93 | } 94 | ); 95 | 96 | const counterStyle = { 97 | display: 'flex', 98 | cursor: 'pointer', 99 | alignItems: 'center', 100 | }; 101 | 102 | export default ReactionCounter; 103 | -------------------------------------------------------------------------------- /src/components/custom/ReactionCounterEmoji.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface ReactionCounterEmojiProps { 4 | node: JSX.Element; 5 | bg: string; 6 | iconSize: number; 7 | index: number; 8 | } 9 | 10 | export const ReactionCounterEmoji = React.forwardRef< 11 | HTMLDivElement, 12 | ReactionCounterEmojiProps 13 | >(({ node, bg, iconSize, index }, ref) => { 14 | const emojiContainerStyle: React.CSSProperties = React.useMemo(() => { 15 | return { 16 | zIndex: index, 17 | position: 'relative', 18 | boxShadow: `0 0 0 2px ${bg}`, 19 | width: `${iconSize}px`, 20 | height: `${iconSize}px`, 21 | }; 22 | }, [iconSize, index, bg]); 23 | 24 | const emojiStyle: React.CSSProperties = React.useMemo(() => { 25 | return { 26 | width: `${iconSize}px`, 27 | height: `${iconSize}px`, 28 | objectFit: 'contain', 29 | objectPosition: 'center center', 30 | }; 31 | }, [iconSize]); 32 | 33 | return ( 34 |
35 | {React.cloneElement(node, { 36 | style: { ...emojiStyle, ...node.props.style }, 37 | })} 38 |
39 | ); 40 | }); 41 | 42 | export default ReactionCounterEmoji; 43 | -------------------------------------------------------------------------------- /src/components/custom/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ReactionBarSelector } from './ReactionBarSelector'; 2 | export { default as ReactionBarSelectorEmoji } from './ReactionBarSelectorEmoji'; 3 | export { default as ReactionCounter } from './ReactionCounter'; 4 | -------------------------------------------------------------------------------- /src/components/facebook/FacebookCounter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CounterObject, groupBy, listOfNames } from '../../helpers'; 3 | import FacebookCounterReaction from './FacebookCounterReaction'; 4 | 5 | export interface FacebookCounterProps { 6 | counters?: CounterObject[]; 7 | user?: string; 8 | important?: string[]; 9 | onClick?: () => void; 10 | bg?: string; 11 | variant?: 'facebook' | 'pokemon'; 12 | alwaysShowOthers?: boolean; 13 | } 14 | 15 | export const FacebookCounter = React.forwardRef< 16 | HTMLDivElement, 17 | FacebookCounterProps 18 | >( 19 | ( 20 | { 21 | counters = defaultProps.counters, 22 | user = defaultProps.user, 23 | important = defaultProps.important, 24 | onClick = defaultProps.onClick, 25 | bg = defaultProps.bg, 26 | variant = defaultProps.variant, 27 | alwaysShowOthers = defaultProps.alwaysShowOthers, 28 | }, 29 | ref 30 | ) => { 31 | const groups = groupBy(counters, 'emoji'); 32 | const names = counters.map(({ by }: CounterObject) => { 33 | return by; 34 | }); 35 | 36 | const nameString = []; 37 | if (names.includes(user)) { 38 | nameString.push('You'); 39 | } 40 | if (important?.length) { 41 | if (names.includes(important[0])) { 42 | nameString.push(important[0]); 43 | } 44 | if (names.includes(important[1])) { 45 | nameString.push(important[1]); 46 | } 47 | } 48 | const othersCount = names.length - nameString.length; 49 | if (alwaysShowOthers || othersCount > 0) { 50 | nameString.push(`${othersCount} other${othersCount === 1 ? '' : 's'}`); 51 | } 52 | 53 | return ( 54 |
55 | {Object.keys(groups).map((reaction, i, reactions) => { 56 | return ( 57 | 64 | ); 65 | })} 66 |
{listOfNames(nameString)}
67 |
68 | ); 69 | } 70 | ); 71 | 72 | export const defaultProps: Required = { 73 | important: [], 74 | bg: '#fff', 75 | variant: 'facebook', 76 | counters: [ 77 | { 78 | emoji: 'like', 79 | by: 'Case Sandberg', 80 | }, 81 | { 82 | emoji: 'like', 83 | by: 'Jon', 84 | }, 85 | { 86 | emoji: 'haha', 87 | by: 'Charlie', 88 | }, 89 | ], 90 | user: 'Charlie', 91 | onClick: () => { 92 | console.log('click'); 93 | }, 94 | alwaysShowOthers: false, 95 | }; 96 | 97 | const counterStyle = { 98 | display: 'flex', 99 | cursor: 'pointer', 100 | color: '#365899', 101 | fontFamily: `"San Francisco", -apple-system, BlinkMacSystemFont, 102 | ".SFNSText-Regular", sans-serif`, 103 | fontSize: '12px', 104 | fontWeight: 500, 105 | }; 106 | const nameStyle = { 107 | paddingLeft: '4px', 108 | marginTop: '2px', 109 | }; 110 | 111 | export default FacebookCounter; 112 | -------------------------------------------------------------------------------- /src/components/facebook/FacebookCounterReaction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import icons from '../../helpers/icons'; 3 | 4 | export interface FacebookCounterReactionProps { 5 | reaction: string; 6 | bg: string; 7 | index: number; 8 | variant: 'facebook' | 'pokemon'; 9 | } 10 | 11 | export const FacebookCounterReaction = React.forwardRef< 12 | HTMLDivElement, 13 | FacebookCounterReactionProps 14 | >(({ reaction, bg, index, variant }, ref) => { 15 | const reactionStyle: React.CSSProperties = React.useMemo(() => { 16 | return { 17 | width: '16px', 18 | height: '16px', 19 | backgroundSize: '100% 100%', 20 | borderRadius: '8px', 21 | backgroundImage: `url(${icons.find(variant, reaction)})`, 22 | boxShadow: `0 0 0 2px ${bg}`, 23 | position: 'relative', 24 | zIndex: index, 25 | }; 26 | }, [reaction, bg, index, variant]); 27 | 28 | return
; 29 | }); 30 | 31 | export default FacebookCounterReaction; 32 | -------------------------------------------------------------------------------- /src/components/facebook/FacebookSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { icons } from '../../helpers'; 3 | import FacebookSelectorEmoji from './FacebookSelectorEmoji'; 4 | 5 | // TODO: use the custom reaction bar 6 | export interface FacebookSelectorProps { 7 | iconSize?: number; 8 | reactions?: string[]; 9 | variant?: 'facebook' | 'pokemon'; 10 | onSelect?: (label: string) => void; 11 | } 12 | 13 | export const FacebookSelector = React.forwardRef< 14 | HTMLDivElement, 15 | FacebookSelectorProps 16 | >( 17 | ( 18 | { 19 | iconSize = defaultProps.iconSize, 20 | reactions = defaultProps.reactions, 21 | variant = defaultProps.variant, 22 | onSelect = defaultProps.onSelect, 23 | }, 24 | ref 25 | ) => { 26 | const emojiStyle = React.useMemo(() => { 27 | return { width: `${iconSize + 10}px` }; 28 | }, [iconSize]); 29 | 30 | return ( 31 |
32 | {reactions.map((reaction: string) => { 33 | return ( 34 |
35 | 40 |
41 | ); 42 | })} 43 |
44 | ); 45 | } 46 | ); 47 | 48 | export const defaultProps: Required = { 49 | reactions: ['like', 'love', 'haha', 'wow', 'sad', 'angry'], 50 | iconSize: 38, 51 | variant: 'facebook', 52 | onSelect: (label: string) => { 53 | console.log(label); 54 | }, 55 | }; 56 | 57 | const wrapStyle = { 58 | backgroundColor: '#fff', 59 | borderRadius: '50px', 60 | padding: '2px', 61 | boxShadow: '0 0 0 1px rgba(0, 0, 0, .05), 0 1px 2px rgba(0, 0, 0, .15)', 62 | display: 'flex', 63 | }; 64 | 65 | export default FacebookSelector; 66 | -------------------------------------------------------------------------------- /src/components/facebook/FacebookSelectorEmoji.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Hover, HoverStyle, withActive } from '../../helpers'; 3 | 4 | export interface FacebookSelectorEmojiProps { 5 | icon: string; 6 | label: string; 7 | onSelect: (label: string) => void; 8 | } 9 | 10 | export const FacebookSelectorEmoji = withActive( 11 | ({ icon, label, onSelect }) => { 12 | const iconStyle = React.useMemo(() => { 13 | return { 14 | paddingBottom: '100%', 15 | backgroundImage: `url(${icon})`, 16 | backgroundSize: '100% 100%', 17 | transformOrigin: 'bottom', 18 | cursor: 'pointer', 19 | transition: '200ms transform cubic-bezier(0.23, 1, 0.32, 1)', 20 | }; 21 | }, [icon]); 22 | 23 | const handleClick = () => { 24 | onSelect && onSelect(label); 25 | }; 26 | 27 | return ( 28 | 29 | 30 | {label} 31 | 32 | 37 | 38 | ); 39 | } 40 | ); 41 | 42 | const wrapStyle: React.CSSProperties = { 43 | padding: '5px', 44 | position: 'relative', 45 | }; 46 | const labelStyleHover = { 47 | transform: 'translateX(-50%) translateY(-10px)', 48 | opacity: '1', 49 | }; 50 | const labelStyle: React.CSSProperties = { 51 | position: 'absolute', 52 | top: '-22px', 53 | background: 'rgba(0,0,0,.8)', 54 | borderRadius: '14px', 55 | color: '#fff', 56 | fontSize: '11px', 57 | padding: '4px 7px 3px', 58 | fontWeight: 'bold', 59 | textTransform: 'capitalize', 60 | left: '50%', 61 | transform: 'translateX(-50%)', 62 | transition: '200ms transform cubic-bezier(0.23, 1, 0.32, 1)', 63 | opacity: '0', 64 | }; 65 | const iconStyleHover = { transform: 'scale(1.3)' }; 66 | 67 | export default FacebookSelectorEmoji; 68 | -------------------------------------------------------------------------------- /src/components/facebook/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FacebookCounter } from './FacebookCounter'; 2 | export { default as FacebookCounterReaction } from './FacebookCounterReaction'; 3 | export { default as FacebookSelector } from './FacebookSelector'; 4 | export { default as FacebookSelectorEmoji } from './FacebookSelectorEmoji'; 5 | -------------------------------------------------------------------------------- /src/components/github/GithubCounter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CounterObject, groupBy, Hover, HoverStyle } from '../../helpers'; 3 | import GithubCounterGroup from './GithubCounterGroup'; 4 | 5 | export interface GithubCounterProps { 6 | counters?: CounterObject[]; 7 | user?: string; 8 | onSelect?: (emoji: string) => void; 9 | onAdd?: () => void; 10 | } 11 | 12 | export const GithubCounter = React.forwardRef< 13 | HTMLDivElement, 14 | GithubCounterProps 15 | >( 16 | ( 17 | { 18 | counters = defaultProps.counters, 19 | user = defaultProps.user, 20 | onSelect = defaultProps.onSelect, 21 | onAdd = defaultProps.onAdd, 22 | }, 23 | ref 24 | ) => { 25 | const groups = groupBy(counters, 'emoji'); 26 | 27 | return ( 28 | 29 | {Object.keys(groups).map((emoji: string) => { 30 | const names = groups[emoji].map(({ by }: CounterObject) => { 31 | return by; 32 | }); 33 | return ( 34 | 42 | ); 43 | })} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | ); 56 | 57 | export const defaultProps: Required = { 58 | counters: [ 59 | { 60 | emoji: '👍', 61 | by: 'Case Sandberg', 62 | }, 63 | { 64 | emoji: '👎', 65 | by: 'Charlie', 66 | }, 67 | ], 68 | user: 'Charlie', 69 | onAdd: () => { 70 | console.log('add'); 71 | }, 72 | onSelect: (emoji: string) => { 73 | console.log(emoji); 74 | }, 75 | }; 76 | 77 | const counterStyle = { 78 | height: '36px', 79 | border: '1px solid #ddd', 80 | borderRadius: '4px', 81 | display: 'flex', 82 | background: '#fff', 83 | }; 84 | const addStyle = { 85 | fill: '#4078c0', 86 | width: '25px', 87 | height: '20px', 88 | padding: '8px 15px', 89 | display: 'flex', 90 | justifyContent: 'space-between', 91 | alignItems: 'center', 92 | cursor: 'pointer', 93 | opacity: '0', 94 | transition: 'opacity 0.1s ease-in-out', 95 | }; 96 | const addStyleHover = { opacity: '1' }; 97 | 98 | export default GithubCounter; 99 | -------------------------------------------------------------------------------- /src/components/github/GithubCounterGroup.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | 3 | import React from 'react'; 4 | import { Hover, HoverStyle } from '../../helpers'; 5 | import { listOfNames } from '../../helpers/strings'; 6 | 7 | export interface GithubCounterGroupProps { 8 | emoji: string; 9 | count: number; 10 | onSelect: (emoji: string) => void; 11 | names: string[]; 12 | active: boolean; 13 | } 14 | 15 | export const GithubCounterGroup = React.forwardRef< 16 | HTMLDivElement, 17 | GithubCounterGroupProps 18 | >(({ emoji, count, onSelect, names, active }, ref) => { 19 | const handleClick = () => { 20 | onSelect(emoji); 21 | }; 22 | 23 | return ( 24 | 29 | {emoji} {count} 30 | 31 | {listOfNames(names)} 32 | 33 | 34 | ); 35 | }); 36 | 37 | const groupStyle: React.CSSProperties = { 38 | width: '35px', 39 | height: '20px', 40 | padding: '8px 15px', 41 | borderRight: '1px solid #ddd', 42 | fontFamily: 43 | '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica', 44 | fontSize: '14px', 45 | color: '#3D76C2', 46 | fontWeight: 500, 47 | display: 'flex', 48 | justifyContent: 'space-between', 49 | alignItems: 'center', 50 | position: 'relative', 51 | cursor: 'pointer', 52 | borderRadius: '3px 0 0 3px', 53 | }; 54 | const groupStyleActive = { 55 | background: '#f2f8fa', 56 | }; 57 | const emojiStyle = { 58 | fontSize: '21px', 59 | marginTop: '1px', 60 | }; 61 | const tooltipStyle: React.CSSProperties = { 62 | maxWidth: '250px', 63 | wordBreak: 'break-word', 64 | wordWrap: 'normal', 65 | whiteSpace: 'nowrap', 66 | font: `normal normal 11px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", 67 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`, 68 | color: '#fff', 69 | background: 'rgba(0,0,0,0.8)', 70 | borderRadius: '3px', 71 | padding: '5px 8px', 72 | position: 'absolute', 73 | top: '100%', 74 | left: '15px', 75 | marginTop: '4px', 76 | opacity: '0', 77 | transition: 'opacity 0.1s ease-in-out', 78 | }; 79 | const tooltipStyleHover = { opacity: '1' }; 80 | 81 | export default GithubCounterGroup; 82 | -------------------------------------------------------------------------------- /src/components/github/GithubSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GithubSelectorEmoji from './GithubSelectorEmoji'; 3 | 4 | export interface GithubSelectorProps { 5 | reactions?: string[]; 6 | onSelect?: (shortCode: string) => void; 7 | } 8 | 9 | export const GithubSelector = React.forwardRef< 10 | HTMLDivElement, 11 | GithubSelectorProps 12 | >( 13 | ( 14 | { reactions = defaultProps.reactions, onSelect = defaultProps.onSelect }, 15 | ref 16 | ) => { 17 | return ( 18 |
19 |

Pick your reaction

20 |
21 |
22 | {reactions.map((reaction: string) => { 23 | return ( 24 | 29 | ); 30 | })} 31 |
32 |
33 | ); 34 | } 35 | ); 36 | 37 | export const defaultProps: Required = { 38 | reactions: ['👍', '👎', '😄', '🎉', '😕', '❤️'], 39 | onSelect: (shortCode: string) => { 40 | console.log(shortCode); 41 | }, 42 | }; 43 | 44 | const selectorStyle = { 45 | paddingTop: '5px', 46 | backgroundColor: '#fff', 47 | border: '1px solid rgba(0,0,0,0.15)', 48 | borderRadius: '4px', 49 | boxShadow: '0 3px 12px rgba(0,0,0,0.15)', 50 | display: 'inline-block', 51 | }; 52 | const labelStyle = { 53 | fontSize: '14px', 54 | lineHeight: '1.5', 55 | color: '#767676', 56 | margin: '6px 12px', 57 | fontFamily: 58 | '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica', 59 | }; 60 | const dividerStyle = { 61 | height: '1px', 62 | margin: '8px 1px 0px', 63 | backgroundColor: '#e5e5e5', 64 | }; 65 | const emojiStyle = { 66 | display: 'flex', 67 | margin: '0 6px', 68 | }; 69 | 70 | export default GithubSelector; 71 | -------------------------------------------------------------------------------- /src/components/github/GithubSelectorEmoji.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Hover, HoverStyle, withActive } from '../../helpers'; 3 | 4 | export interface GithubSelectorEmojiProps { 5 | onSelect: (shortCode: string) => void; 6 | shortCode: string; 7 | active?: boolean; 8 | } 9 | 10 | export const GithubSelectorEmoji = withActive( 11 | ({ onSelect, shortCode, active = false }) => { 12 | const handleClick = () => { 13 | onSelect(shortCode); 14 | }; 15 | 16 | return ( 17 | 21 | 22 | {shortCode} 23 | 24 | 25 | ); 26 | } 27 | ); 28 | 29 | const wrapStyle = { 30 | padding: '8px 0', 31 | }; 32 | const emojiStyle: React.CSSProperties = { 33 | width: '34px', 34 | textAlign: 'center', 35 | lineHeight: '29px', 36 | fontSize: '21px', 37 | fontFamily: 38 | '"Apple Color Emoji", "Segoe UI", "Segoe UI Emoji", "Segoe UI Symbol"', 39 | cursor: 'pointer', 40 | 41 | transform: 'scale(1)', 42 | transition: 'transform 0.15s cubic-bezier(0.2, 0, 0.13, 2)', 43 | }; 44 | const emojiStyleHover = { 45 | transform: 'scale(1.2)', 46 | }; 47 | const wrapStyleActive = { 48 | backgroundColor: '#f2f8fa', 49 | }; 50 | 51 | export default GithubSelectorEmoji; 52 | -------------------------------------------------------------------------------- /src/components/github/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GithubCounter } from './GithubCounter'; 2 | export { default as GithubCounterGroup } from './GithubCounterGroup'; 3 | export { default as GithubSelector } from './GithubSelector'; 4 | export { default as GithubSelectorEmoji } from './GithubSelectorEmoji'; 5 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './custom'; 2 | export * from './facebook'; 3 | export * from './github'; 4 | export * from './pokemon'; 5 | export * from './slack'; 6 | export * from './youtube'; 7 | -------------------------------------------------------------------------------- /src/components/pokemon/PokemonCounter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FacebookCounter, { 3 | FacebookCounterProps, 4 | } from '../facebook/FacebookCounter'; 5 | 6 | export type PokemonCounterProps = Omit; 7 | 8 | export const PokemonCounter = React.forwardRef< 9 | HTMLDivElement, 10 | PokemonCounterProps 11 | >((props, ref) => { 12 | return ; 13 | }); 14 | 15 | export default PokemonCounter; 16 | -------------------------------------------------------------------------------- /src/components/pokemon/PokemonSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FacebookSelector, { 3 | defaultProps, 4 | FacebookSelectorProps, 5 | } from '../facebook/FacebookSelector'; 6 | 7 | export type PokemonSelectorProps = Omit; 8 | 9 | export const PokemonSelector = React.forwardRef< 10 | HTMLDivElement, 11 | PokemonSelectorProps 12 | >( 13 | ( 14 | { 15 | reactions = defaultProps.reactions, 16 | iconSize = defaultProps.iconSize, 17 | onSelect = defaultProps.onSelect, 18 | }, 19 | ref 20 | ) => { 21 | return ( 22 | 29 | ); 30 | } 31 | ); 32 | 33 | export default PokemonSelector; 34 | -------------------------------------------------------------------------------- /src/components/pokemon/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PokemonCounter } from './PokemonCounter'; 2 | export { default as PokemonSelector } from './PokemonSelector'; 3 | -------------------------------------------------------------------------------- /src/components/slack/SlackCSS.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const SlackCSS = () => { 4 | return ( 5 |