├── .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 | # 
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 | [](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 | 
59 |
60 | Also works with images.
61 |
62 | 
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 | 
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 |
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 |
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 |
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 |
48 |
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 |
34 | );
35 | };
36 |
37 | export default SlackCSS;
38 |
--------------------------------------------------------------------------------
/src/components/slack/SlackCounter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CounterObject, groupBy, Hover, HoverStyle } from '../../helpers';
3 | import SlackCounterGroup from './SlackCounterGroup';
4 | import SlackCSS from './SlackCSS';
5 |
6 | export interface SlackCounterProps {
7 | counters?: CounterObject[];
8 | user?: string;
9 | onSelect?: (emoji: string) => void;
10 | onAdd?: () => void;
11 | }
12 |
13 | export const SlackCounter = React.forwardRef(
14 | (
15 | {
16 | counters = defaultProps.counters,
17 | user = defaultProps.user,
18 | onSelect = defaultProps.onSelect,
19 | onAdd = defaultProps.onAdd,
20 | },
21 | ref
22 | ) => {
23 | const groups = groupBy(counters, 'emoji');
24 |
25 | return (
26 | <>
27 |
28 |
29 | {Object.keys(groups).map((emoji: string) => {
30 | const names = groups[emoji].map(({ by }: CounterObject) => {
31 | return by;
32 | });
33 | return (
34 |
35 |
42 |
43 | );
44 | })}
45 |
50 |
51 |
52 |
53 | >
54 | );
55 | }
56 | );
57 |
58 | export const defaultProps: Required = {
59 | counters: [
60 | {
61 | emoji: '👍',
62 | by: 'Case Sandberg',
63 | },
64 | {
65 | emoji: '👎',
66 | by: 'Charlie!!!!!',
67 | },
68 | ],
69 | user: 'Charlie',
70 | onSelect: (emoji: string) => {
71 | console.log(emoji);
72 | },
73 | onAdd: () => {
74 | console.log('add');
75 | },
76 | };
77 |
78 | const counterStyle = {
79 | display: 'flex',
80 | };
81 | const addStyle = {
82 | cursor: 'pointer',
83 | fontFamily: 'Slack',
84 | paddingLeft: '8px',
85 | opacity: '0',
86 | transition: 'opacity 0.1s ease-in-out',
87 | };
88 | const groupStyle = {
89 | marginRight: '4px',
90 | };
91 | const addStyleHover = {
92 | opacity: '1',
93 | };
94 |
95 | export default SlackCounter;
96 |
--------------------------------------------------------------------------------
/src/components/slack/SlackCounterGroup.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Hover, HoverStyle, listOfNames } from '../../helpers';
3 |
4 | export interface SlackCounterGroupProps {
5 | hover?: boolean;
6 | onSelect?: (emoji: string) => void;
7 | emoji: string;
8 | count?: number;
9 | active?: boolean;
10 | names?: string[];
11 | }
12 | export const SlackCounterGroup = React.forwardRef<
13 | HTMLDivElement,
14 | SlackCounterGroupProps
15 | >(({ onSelect, emoji, count, names, active }, ref) => {
16 | const handleClick = () => {
17 | onSelect && onSelect(emoji);
18 | };
19 |
20 | return (
21 |
27 |
28 | {emoji}
29 | {' '}
30 | {count}
31 | {names ? (
32 |
33 | {listOfNames(names)}
34 |
35 | ) : null}
36 |
37 | );
38 | });
39 |
40 | const groupStyle: React.CSSProperties = {
41 | height: '19px',
42 | paddingTop: '1px',
43 | paddingLeft: '3px',
44 | paddingRight: '4px',
45 | border: '1px solid #E8E8E8',
46 | background: '#fff',
47 | fontSize: '11px',
48 | color: '#999',
49 | fontWeight: 500,
50 | display: 'flex',
51 | justifyContent: 'space-between',
52 | alignItems: 'center',
53 | position: 'relative',
54 | cursor: 'pointer',
55 | borderRadius: '5px',
56 | };
57 | const emojiStyle = {
58 | fontSize: '16px',
59 | marginTop: '1px',
60 | paddingRight: '3px',
61 | };
62 | const tooltipStyle: React.CSSProperties = {
63 | maxWidth: '250px',
64 | wordBreak: 'break-word',
65 | wordWrap: 'normal',
66 | whiteSpace: 'nowrap',
67 | font: `normal normal 11px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI",
68 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`,
69 | color: '#fff',
70 | background: 'rgba(0,0,0,0.8)',
71 | borderRadius: '3px',
72 | padding: '5px 8px',
73 | position: 'absolute',
74 | bottom: '100%',
75 | left: '50%',
76 | transform: 'translateX(-50%)',
77 | marginBottom: '4px',
78 | opacity: '0',
79 | transition: 'opacity 0.1s ease-in-out',
80 | };
81 | const tooltipStyleHover = {
82 | opacity: '1',
83 | };
84 | const groupStyleActive = {
85 | background: '#F4FAFF',
86 | border: '1px solid #BBE1FF',
87 | };
88 | const emojiStyleNoNames = {
89 | paddingRight: '0',
90 | };
91 |
92 | export default SlackCounterGroup;
93 |
--------------------------------------------------------------------------------
/src/components/slack/SlackSelector.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SlackCSS from './SlackCSS';
3 | import SlackSelectorFooter from './SlackSelectorFooter';
4 | import SlackSelectorHeader from './SlackSelectorHeader';
5 | import SlackSelectorItems from './SlackSelectorItems';
6 |
7 | export interface SlackSelectorProps {
8 | scrollHeight?: string;
9 | frequent?: string[];
10 | removeEmojis?: string[];
11 | onSelect?: (id: string) => void;
12 | }
13 |
14 | export const SlackSelector = React.forwardRef<
15 | HTMLDivElement,
16 | SlackSelectorProps
17 | >(
18 | (
19 | {
20 | scrollHeight = defaultProps.scrollHeight,
21 | frequent = defaultProps.frequent,
22 | removeEmojis = defaultProps.removeEmojis,
23 | onSelect = defaultProps.onSelect,
24 | },
25 | ref
26 | ) => {
27 | return (
28 |
29 |
30 |
31 |
37 |
38 |
39 | );
40 | }
41 | );
42 |
43 | const menuStyle = {
44 | fontFamily: '"Helvetica Neue",Helvetica,"Segoe UI",Tahoma,Arial,sans-serif',
45 | width: '358px',
46 | color: '#555459',
47 | fontSize: '.95rem',
48 | background: '#f7f7f7',
49 | lineHeight: '1rem',
50 | boxShadow: '0 5px 10px rgba(0,0,0,.12)',
51 | borderRadius: '6px',
52 | border: '1px solid rgba(0,0,0,.15)',
53 | };
54 |
55 | export const defaultProps: Required = {
56 | scrollHeight: '270px',
57 | removeEmojis: [
58 | '🙂',
59 | '🙃',
60 | '☺️',
61 | '🤑',
62 | '🤓',
63 | '🤗',
64 | '🙄',
65 | '🤔',
66 | '🙁',
67 | '☹️',
68 | '🤐',
69 | '🤒',
70 | '🤕',
71 | '🤖',
72 | ],
73 | frequent: [
74 | '👍',
75 | '🐉',
76 | '🙌',
77 | '🗿',
78 | '😊',
79 | '🐬',
80 | '😹',
81 | '👻',
82 | '🚀',
83 | '🚁',
84 | '🏇',
85 | '🇨🇦',
86 | ],
87 | onSelect: (id: string) => {
88 | console.log(id);
89 | },
90 | };
91 |
92 | export default SlackSelector;
93 |
--------------------------------------------------------------------------------
/src/components/slack/SlackSelectorFooter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SlackSelectorSection from './SlackSelectorSection';
3 |
4 | export interface SlackSelectorFooterProps {
5 | onSelect: (id: string) => void;
6 | }
7 |
8 | export const SlackSelectorFooter = React.forwardRef<
9 | HTMLDivElement,
10 | SlackSelectorFooterProps
11 | >(({ onSelect }, ref) => {
12 | return (
13 |
14 |
Handy Reactions
15 |
16 |
20 |
21 |
22 | );
23 | });
24 |
25 | const footerStyle = {
26 | padding: '5px 11px',
27 | borderTop: '1px solid rgba(0,0,0,.15)',
28 | display: 'flex',
29 | justifyContent: 'space-between',
30 | };
31 | const leftStyle: React.CSSProperties = {
32 | fontSize: '16px',
33 | lineHeight: '1.5',
34 | margin: '4px 2px',
35 | fontWeight: 600,
36 | WebkitFontSmoothing: 'antialiased',
37 | };
38 | const rightStyle = {
39 | paddingRight: '6px',
40 | };
41 |
42 | export default SlackSelectorFooter;
43 |
--------------------------------------------------------------------------------
/src/components/slack/SlackSelectorHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SlackSelectorHeaderTab from './SlackSelectorHeaderTab';
3 |
4 | interface Tab {
5 | icon: string;
6 | id: string;
7 | }
8 |
9 | export interface SlackSelectorHeaderProps {
10 | tabs?: Tab[];
11 | }
12 |
13 | export const SlackSelectorHeader = React.forwardRef<
14 | HTMLDivElement,
15 | SlackSelectorHeaderProps
16 | >(({ tabs = [] }, ref) => {
17 | const [activeString, setActiveString] = React.useState('');
18 |
19 | const handleClick = (id: string) => {
20 | document?.getElementById(id)!.scrollIntoView(false);
21 | setActiveString(id);
22 | };
23 |
24 | return (
25 |
26 | {tabs.map((tab: Tab) => {
27 | return (
28 |
35 | );
36 | })}
37 |
38 | );
39 | });
40 |
41 | const headerStyle = {
42 | padding: '4px 0 0 7px',
43 | borderBottom: '1px solid rgba(0,0,0,.15)',
44 | display: 'flex',
45 | };
46 |
47 | SlackSelectorHeader.defaultProps = {
48 | tabs: [
49 | {
50 | icon: '',
51 | id: 'mine',
52 | },
53 | {
54 | icon: '',
55 | id: 'people',
56 | },
57 | {
58 | icon: '',
59 | id: 'nature',
60 | },
61 | {
62 | icon: '',
63 | id: 'food-and-drink',
64 | },
65 | {
66 | icon: '',
67 | id: 'activity',
68 | },
69 | {
70 | icon: '',
71 | id: 'travel-and-places',
72 | },
73 | {
74 | icon: '',
75 | id: 'objects',
76 | },
77 | {
78 | icon: '',
79 | id: 'symbols',
80 | },
81 | {
82 | icon: '',
83 | id: 'flags',
84 | },
85 | ],
86 | };
87 |
88 | export default SlackSelectorHeader;
89 |
--------------------------------------------------------------------------------
/src/components/slack/SlackSelectorHeaderTab.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Hover } from '../../helpers';
3 |
4 | export interface SlackSelectorHeaderTabProps {
5 | onClick: (id: string) => void;
6 | id: string;
7 | icon: string;
8 | active?: boolean;
9 | }
10 |
11 | export const SlackSelectorHeaderTab = React.forwardRef<
12 | HTMLDivElement,
13 | SlackSelectorHeaderTabProps
14 | >(({ onClick, id, icon, active = false }, ref) => {
15 | const handleClick = () => {
16 | onClick && onClick(id);
17 | };
18 |
19 | return (
20 |
26 | {icon}
27 |
28 | );
29 | });
30 |
31 | const tabStyle = {
32 | color: '#9e9ea6',
33 | padding: '5px 8px 7px',
34 | borderRadius: '6px 6px 0 0',
35 | marginRight: '1px',
36 | borderBottom: '3px solid transparent',
37 | cursor: 'pointer',
38 | };
39 | const iconStyle = {
40 | width: '20px',
41 | height: '18px',
42 | fontFamily: 'Slack',
43 | fontSize: '20px',
44 | WebkitFontSmoothing: 'antialiased',
45 | };
46 | const tabStyleHover = {
47 | color: '#555459',
48 | borderBottom: '3px solid #2ab27b',
49 | };
50 | const tabStyleActive = {
51 | color: '#9e9ea6',
52 | borderBottom: '3px solid #2ab27b',
53 | };
54 |
55 | export default SlackSelectorHeaderTab;
56 |
--------------------------------------------------------------------------------
/src/components/slack/SlackSelectorItems.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import emoji from '../../helpers/emoji';
3 | import SlackSelectorSection from './SlackSelectorSection';
4 |
5 | export interface SlackSelectorItemsProps {
6 | scrollHeight: string;
7 | frequent: string[];
8 | onSelect: (emoji: string) => void;
9 | removeEmojis: string[];
10 | }
11 |
12 | export const SlackSelectorItems = React.forwardRef<
13 | HTMLDivElement,
14 | SlackSelectorItemsProps
15 | >(({ scrollHeight, frequent, onSelect, removeEmojis }, ref) => {
16 | const wrapStyle: React.CSSProperties = React.useMemo(() => {
17 | return {
18 | maxHeight: scrollHeight,
19 | overflowY: 'auto',
20 | overflowX: 'hidden',
21 | padding: '4px 4px 8px',
22 | };
23 | }, [scrollHeight]);
24 |
25 | return (
26 |
27 |
28 | {frequent ? (
29 |
35 | ) : null}
36 | {Object.keys(emoji).map((slug: string) => {
37 | const group = emoji[slug as keyof typeof emoji];
38 | return (
39 |
49 | );
50 | })}
51 |
52 |
53 | );
54 | });
55 |
56 | const sectionsStyle = {
57 | padding: '4px 4px 0',
58 | background: '#fff',
59 | };
60 |
61 | export default SlackSelectorItems;
62 |
--------------------------------------------------------------------------------
/src/components/slack/SlackSelectorSection.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { emojiColors, sectionSlugToName } from '../../helpers/slack';
3 | import SlackSelectorSectionEmoji from './SlackSelectorSectionEmoji';
4 |
5 | export interface SlackSelectorSectionProps {
6 | slug?: string;
7 | emojis: string[];
8 | onSelect: (emoji: string) => void;
9 | }
10 |
11 | export const SlackSelectorSection = React.forwardRef<
12 | HTMLDivElement,
13 | SlackSelectorSectionProps
14 | >(({ slug = '', emojis, onSelect }, ref) => {
15 | return (
16 |
17 |
{sectionSlugToName(slug)}
18 |
19 | {emojis.map((emoji, i) => {
20 | return (
21 |
27 | );
28 | })}
29 |
30 |
31 | );
32 | });
33 |
34 | const emojisStyle: React.CSSProperties = {
35 | display: 'flex',
36 | flexWrap: 'wrap',
37 | };
38 | const titleStyle: React.CSSProperties = {
39 | fontWeight: 600,
40 | WebkitFontSmoothing: 'antialiased',
41 | fontSize: '16px',
42 | lineHeight: '1.5rem',
43 | margin: '0 6px',
44 | };
45 |
46 | export default SlackSelectorSection;
47 |
--------------------------------------------------------------------------------
/src/components/slack/SlackSelectorSectionEmoji.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Hover } from '../../helpers';
3 |
4 | export interface SlackSelectorSectionEmojiProps {
5 | hoverColor: string;
6 | onSelect: (emoji: string) => void;
7 | emoji: string;
8 | }
9 |
10 | export const SlackSelectorSectionEmoji = React.forwardRef<
11 | HTMLDivElement,
12 | SlackSelectorSectionEmojiProps
13 | >(({ hoverColor, onSelect, emoji }, ref) => {
14 | const wrapStyleHover = React.useMemo(() => {
15 | return { background: hoverColor };
16 | }, [hoverColor]);
17 |
18 | const handleClick = () => {
19 | onSelect(emoji);
20 | };
21 |
22 | return (
23 |
29 | {emoji}
30 |
31 | );
32 | });
33 |
34 | const wrapStyle = {
35 | width: '36px',
36 | height: '32px',
37 | display: 'flex',
38 | justifyContent: 'center',
39 | alignItems: 'center',
40 | margin: '0 1px 1px 0',
41 | borderRadius: '6px',
42 | cursor: 'pointer',
43 | transition: 'background .15s ease-out 50ms',
44 | };
45 | const emojiStyle = {
46 | fontSize: '22px',
47 | width: '22px',
48 | height: '22px',
49 | lineHeight: '26px',
50 | };
51 |
52 | export default SlackSelectorSectionEmoji;
53 |
--------------------------------------------------------------------------------
/src/components/slack/fonts/slack-icons-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charkour/react-reactions/bf2b92846ec49d882678e0eb869190176e71d086/src/components/slack/fonts/slack-icons-Regular.eot
--------------------------------------------------------------------------------
/src/components/slack/fonts/slack-icons-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charkour/react-reactions/bf2b92846ec49d882678e0eb869190176e71d086/src/components/slack/fonts/slack-icons-Regular.ttf
--------------------------------------------------------------------------------
/src/components/slack/fonts/slack-icons-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charkour/react-reactions/bf2b92846ec49d882678e0eb869190176e71d086/src/components/slack/fonts/slack-icons-Regular.woff
--------------------------------------------------------------------------------
/src/components/slack/index.ts:
--------------------------------------------------------------------------------
1 | export { default as SlackCounter } from './SlackCounter';
2 | export { default as SlackCounterGroup } from './SlackCounterGroup';
3 | export { default as SlackSelector } from './SlackSelector';
4 | export { default as SlackSelectorFooter } from './SlackSelectorFooter';
5 | export { default as SlackSelectorHeader } from './SlackSelectorHeader';
6 | export { default as SlackSelectorHeaderTab } from './SlackSelectorHeaderTab';
7 | export { default as SlackSelectorItems } from './SlackSelectorItems';
8 | export { default as SlackSelectorSection } from './SlackSelectorSection';
9 | export { default as SlackSelectorSectionEmoji } from './SlackSelectorSectionEmoji';
10 |
--------------------------------------------------------------------------------
/src/components/youtube/YoutubeCounter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import YoutubeCounterButton from './YoutubeCounterButton';
3 |
4 | export interface YoutubeCounterProps {
5 | like?: string;
6 | dislike?: string;
7 | onLikeClick?: () => void;
8 | onDislikeClick?: () => void;
9 | didLike?: boolean;
10 | didDislike?: boolean;
11 | }
12 |
13 | export const YoutubeCounter = React.forwardRef<
14 | HTMLDivElement,
15 | YoutubeCounterProps
16 | >(
17 | (
18 | {
19 | like = defaultProps.like,
20 | dislike = defaultProps.dislike,
21 | onLikeClick = defaultProps.onLikeClick,
22 | onDislikeClick = defaultProps.onDislikeClick,
23 | didLike = defaultProps.didLike,
24 | didDislike = defaultProps.didDislike,
25 | },
26 | ref
27 | ) => {
28 | const handleLikeClick = () => onLikeClick();
29 | const handleDislikeClick = () => onDislikeClick();
30 |
31 | return (
32 |
49 | );
50 | }
51 | );
52 |
53 | const defaultProps: Required = {
54 | like: '3',
55 | dislike: '2',
56 | onLikeClick: () => {
57 | console.log('like');
58 | },
59 | onDislikeClick: () => {
60 | console.log('dislike');
61 | },
62 | didLike: false,
63 | didDislike: false,
64 | };
65 |
66 | const counterStyle = {
67 | display: 'flex',
68 | };
69 | const spaceStyle = {
70 | width: '12px',
71 | };
72 |
73 | export default YoutubeCounter;
74 |
--------------------------------------------------------------------------------
/src/components/youtube/YoutubeCounterButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Hover, HoverStyle } from '../../helpers';
3 |
4 | export interface YoutubeCounterButtonProps {
5 | position: string;
6 | number: string;
7 | tooltip: string;
8 | onClick: () => void;
9 | active: boolean;
10 | // TODO: not used, but would be cool
11 | // activeColor: string;
12 | }
13 |
14 | export const YoutubeCounterButton = React.forwardRef<
15 | HTMLDivElement,
16 | YoutubeCounterButtonProps
17 | >(
18 | (
19 | {
20 | position,
21 | number,
22 | tooltip,
23 | onClick,
24 | active,
25 | // activeColor,
26 | },
27 | ref
28 | ) => {
29 | const iconStyle = React.useMemo(() => {
30 | return {
31 | background: `no-repeat url(//s.ytimg.com/yts/imgbin/www-hitchhiker-2x-vflaXbyPz.webp) ${position}`,
32 | backgroundSize: '573px 310px',
33 | width: '20px',
34 | height: '20px',
35 | marginRight: '6px',
36 | };
37 | }, [position]);
38 |
39 | return (
40 |
46 |
47 | {parseInt(number, 10).toLocaleString()}
48 |
49 | {tooltip}
50 |
51 |
52 | );
53 | }
54 | );
55 |
56 | const buttonStyle: React.CSSProperties = {
57 | display: 'flex',
58 | alignItems: 'center',
59 | fontFamily: 'Roboto,arial,sans-serif',
60 | fontSize: '11px',
61 | opacity: '0.5',
62 | cursor: 'pointer',
63 | position: 'relative',
64 | };
65 | const tooltipStyle: React.CSSProperties = {
66 | color: '#fff',
67 | background: 'rgba(0,0,0,0.8)',
68 | borderRadius: '3px',
69 | padding: '5px 8px',
70 | position: 'absolute',
71 | bottom: '100%',
72 | left: '50%',
73 | transform: 'translateX(-50%)',
74 | marginBottom: '4px',
75 | whiteSpace: 'nowrap',
76 | opacity: '0',
77 | transition: 'opacity 0.1s ease-in-out',
78 | };
79 | const buttonStyleHover = {
80 | opacity: '0.7',
81 | };
82 | const tooltipStyleHover = {
83 | opacity: '1',
84 | };
85 | const buttonStyleActive = {
86 | opacity: '1',
87 | };
88 |
89 | export default YoutubeCounterButton;
90 |
--------------------------------------------------------------------------------
/src/components/youtube/index.ts:
--------------------------------------------------------------------------------
1 | export { default as YoutubeCounter } from './YoutubeCounter';
2 | export { default as YoutubeCounterButton } from './YoutubeCounterButton';
3 |
--------------------------------------------------------------------------------
/src/helpers/Hover.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { HoverContext } from './useHover';
3 |
4 | interface HoverProps extends React.HTMLAttributes {
5 | hoverStyle?: React.CSSProperties;
6 | }
7 |
8 | // TODO: turn this into a HOC?
9 | // Wrapper that keeps track of weather or not the component is being hovered
10 | export const Hover = React.forwardRef(
11 | ({ hoverStyle = {}, children, style, ...rest }, ref) => {
12 | const [isHovered, setHovered] = React.useState(false);
13 |
14 | return (
15 |
16 | setHovered(true)}
19 | onMouseLeave={() => setHovered(false)}
20 | {...rest}
21 | style={{ ...style, ...(isHovered ? hoverStyle : {}) }}
22 | >
23 | {children}
24 |
25 |
26 | );
27 | }
28 | );
29 |
30 | export default Hover;
31 |
--------------------------------------------------------------------------------
/src/helpers/HoverStyle.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useHover } from './useHover';
3 |
4 | interface HoverStyleProps extends React.HTMLAttributes {
5 | hoverStyle: React.CSSProperties;
6 | }
7 |
8 | // https://stackoverflow.com/a/65886428/9931154
9 | export const HoverStyle: React.FC = ({
10 | style = {},
11 | hoverStyle,
12 | children,
13 | ...rest
14 | }) => {
15 | const isHovered = useHover();
16 | const calculatedStyle = { ...style, ...(isHovered ? hoverStyle : {}) };
17 |
18 | return (
19 |
20 | {children}
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/helpers/emoji.ts:
--------------------------------------------------------------------------------
1 | export const emoji = {
2 | people: [
3 | '😀',
4 | '😬',
5 | '😁',
6 | '😂',
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 | '😏',
32 | '😶',
33 | '😐',
34 | '😑',
35 | '😒',
36 | '🙄',
37 | '🤔',
38 | '😳',
39 | '😞',
40 | '😟',
41 | '😠',
42 | '😡',
43 | '😔',
44 | '😕',
45 | '🙁',
46 | '☹️',
47 | '😣',
48 | '😖',
49 | '😫',
50 | '😩',
51 | '😤',
52 | '😮',
53 | '😱',
54 | '😨',
55 | '😰',
56 | '😯',
57 | '😦',
58 | '😧',
59 | '😢',
60 | '😥',
61 | '😪',
62 | '😓',
63 | '😭',
64 | '😵',
65 | '😲',
66 | '🤐',
67 | '😷',
68 | '🤒',
69 | '🤕',
70 | '😴',
71 | '💤',
72 | '💩',
73 | '😈',
74 | '👿',
75 | '👹',
76 | '👺',
77 | '💀',
78 | '👻',
79 | '👽',
80 | '🤖',
81 | '😺',
82 | '😸',
83 | '😹',
84 | '😻',
85 | '😼',
86 | '😽',
87 | '🙀',
88 | '😿',
89 | '😾',
90 | '🙌',
91 | '👏',
92 | '👋',
93 | '👍',
94 | '👊',
95 | '✊',
96 | '✌️',
97 | '👌',
98 | '✋',
99 | '💪',
100 | '🙏',
101 | '☝️',
102 | '👆',
103 | '👇',
104 | '👈',
105 | '👉',
106 | '🖕',
107 | '🤘',
108 | '🖖',
109 | '✍️',
110 | '💅',
111 | '👄',
112 | '👅',
113 | '👂',
114 | '👃',
115 | '👁',
116 | '👀',
117 | '👤',
118 | '🗣',
119 | '👶',
120 | '👦',
121 | '👧',
122 | '👨',
123 | '👩',
124 | '👱',
125 | '👴',
126 | '👵',
127 | '👲',
128 | '👳',
129 | '👮',
130 | '👷',
131 | '💂',
132 | '🕵',
133 | '🎅',
134 | '👼',
135 | '👸',
136 | '👰',
137 | '🚶',
138 | '🏃',
139 | '💃',
140 | '👯',
141 | '👫',
142 | '👬',
143 | '👭',
144 | '🙇',
145 | '💁',
146 | '🙅',
147 | '🙆',
148 | '🙋',
149 | '🙎',
150 | '🙍',
151 | '💇',
152 | '💆',
153 | '💑',
154 | '👩❤️👩',
155 | '👨❤️👨',
156 | '💏',
157 | '👩❤️💋👩',
158 | '👨❤️💋👨',
159 | '👪',
160 | '👨👩👧',
161 | '👨👩👧👦',
162 | '👨👩👦👦',
163 | '👨👩👧👧',
164 | '👩👩👦',
165 | '👩👩👧',
166 | '👩👩👧👦',
167 | '👩👩👦👦',
168 | '👩👩👧👧',
169 | '👨👨👦',
170 | '👨👨👧',
171 | '👨👨👧👦',
172 | '👨👨👦👦',
173 | '👨👨👧👧',
174 | '👚',
175 | '👕',
176 | '👖',
177 | '👔',
178 | '👗',
179 | '👙',
180 | '👘',
181 | '💄',
182 | '💋',
183 | '👣',
184 | '👠',
185 | '👡',
186 | '👢',
187 | '👞',
188 | '👟',
189 | '👒',
190 | '🎩',
191 | '⛑',
192 | '🎓',
193 | '👑',
194 | '🎒',
195 | '👝',
196 | '👛',
197 | '👜',
198 | '💼',
199 | '👓',
200 | '🕶',
201 | '💍',
202 | '🌂',
203 | ],
204 | nature: [
205 | '🐶',
206 | '🐱',
207 | '🐭',
208 | '🐹',
209 | '🐰',
210 | '🐻',
211 | '🐼',
212 | '🐨',
213 | '🐯',
214 | '🦁',
215 | '🐮',
216 | '🐷',
217 | '🐽',
218 | '🐸',
219 | '🐙',
220 | '🐵',
221 | '🙈',
222 | '🙉',
223 | '🙊',
224 | '🐒',
225 | '🐔',
226 | '🐧',
227 | '🐦',
228 | '🐤',
229 | '🐣',
230 | '🐥',
231 | '🐺',
232 | '🐗',
233 | '🐴',
234 | '🦄',
235 | '🐝',
236 | '🐛',
237 | '🐌',
238 | '🐞',
239 | '🐜',
240 | '🕷',
241 | '🦂',
242 | '🦀',
243 | '🐍',
244 | '🐢',
245 | '🐠',
246 | '🐟',
247 | '🐡',
248 | '🐬',
249 | '🐳',
250 | '🐋',
251 | '🐊',
252 | '🐆',
253 | '🐅',
254 | '🐃',
255 | '🐂',
256 | '🐄',
257 | '🐪',
258 | '🐫',
259 | '🐘',
260 | '🐐',
261 | '🐏',
262 | '🐑',
263 | '🐎',
264 | '🐖',
265 | '🐀',
266 | '🐁',
267 | '🐓',
268 | '🦃',
269 | '🕊',
270 | '🐕',
271 | '🐩',
272 | '🐈',
273 | '🐇',
274 | '🐿',
275 | '🐾',
276 | '🐉',
277 | '🐲',
278 | '🌵',
279 | '🎄',
280 | '🌲',
281 | '🌳',
282 | '🌴',
283 | '🌱',
284 | '🌿',
285 | '☘',
286 | '🍀',
287 | '🎍',
288 | '🎋',
289 | '🍃',
290 | '🍂',
291 | '🍁',
292 | '🌾',
293 | '🌺',
294 | '🌻',
295 | '🌹',
296 | '🌷',
297 | '🌼',
298 | '🌸',
299 | '💐',
300 | '🍄',
301 | '🌰',
302 | '🎃',
303 | '🐚',
304 | '🕸',
305 | '🌎',
306 | '🌍',
307 | '🌏',
308 | '🌕',
309 | '🌖',
310 | '🌗',
311 | '🌘',
312 | '🌑',
313 | '🌒',
314 | '🌓',
315 | '🌔',
316 | '🌚',
317 | '🌝',
318 | '🌛',
319 | '🌜',
320 | '🌞',
321 | '🌙',
322 | '⭐️',
323 | '🌟',
324 | '💫',
325 | '✨',
326 | '☄',
327 | '☀️',
328 | '🌤',
329 | '⛅️',
330 | '🌥',
331 | '🌦',
332 | '☁️',
333 | '🌧',
334 | '⛈',
335 | '🌩',
336 | '⚡️',
337 | '🔥',
338 | '💥',
339 | '❄️',
340 | '🌨',
341 | '🔥',
342 | '💥',
343 | '❄️',
344 | '🌨',
345 | '☃️',
346 | '⛄️',
347 | '🌬',
348 | '💨',
349 | '🌪',
350 | '🌫',
351 | '☂️',
352 | '☔️',
353 | '💧',
354 | '💦',
355 | '🌊',
356 | ],
357 | 'food-and-drink': [
358 | '🍏',
359 | '🍎',
360 | '🍐',
361 | '🍊',
362 | '🍋',
363 | '🍌',
364 | '🍉',
365 | '🍇',
366 | '🍓',
367 | '🍈',
368 | '🍒',
369 | '🍑',
370 | '🍍',
371 | '🍅',
372 | '🍆',
373 | '🌶',
374 | '🌽',
375 | '🍠',
376 | '🍯',
377 | '🍞',
378 | '🧀',
379 | '🍗',
380 | '🍖',
381 | '🍤',
382 | '🍳',
383 | '🍔',
384 | '🍟',
385 | '🌭',
386 | '🍕',
387 | '🍝',
388 | '🌮',
389 | '🌯',
390 | '🍜',
391 | '🍲',
392 | '🍥',
393 | '🍣',
394 | '🍱',
395 | '🍛',
396 | '🍙',
397 | '🍚',
398 | '🍘',
399 | '🍢',
400 | '🍡',
401 | '🍧',
402 | '🍨',
403 | '🍦',
404 | '🍰',
405 | '🎂',
406 | '🍮',
407 | '🍬',
408 | '🍭',
409 | '🍫',
410 | '🍿',
411 | '🍩',
412 | '🍪',
413 | '🍺',
414 | '🍻',
415 | '🍷',
416 | '🍸',
417 | '🍹',
418 | '🍾',
419 | '🍶',
420 | '🍵',
421 | '☕️',
422 | '🍼',
423 | '🍴',
424 | '🍽',
425 | ],
426 | activity: [
427 | '⚽️',
428 | '🏀',
429 | '🏈',
430 | '⚾️',
431 | '🎾',
432 | '🏐',
433 | '🏉',
434 | '🎱',
435 | '⛳️',
436 | '🏌',
437 | '🏓',
438 | '🏸',
439 | '🏒',
440 | '🏑',
441 | '🏏',
442 | '🎿',
443 | '⛷',
444 | '🏂',
445 | '⛸',
446 | '🏹',
447 | '🎣',
448 | '🚣',
449 | '🏊',
450 | '🏄',
451 | '🛀',
452 | '⛹',
453 | '🏋',
454 | '🚴',
455 | '🚵',
456 | '🏇',
457 | '🕴',
458 | '🏆',
459 | '🎽',
460 | '🏅',
461 | '🎖',
462 | '🎗',
463 | '🏵',
464 | '🎫',
465 | '🎟',
466 | '🎭',
467 | '🎨',
468 | '🎪',
469 | '🎤',
470 | '🎧',
471 | '🎼',
472 | '🎹',
473 | '🎷',
474 | '🎺',
475 | '🎸',
476 | '🎻',
477 | '🎬',
478 | '🎮',
479 | '👾',
480 | '🎯',
481 | '🎲',
482 | '🎰',
483 | '🎳',
484 | ],
485 | 'travel-and-places': [
486 | '🚗',
487 | '🚕',
488 | '🚙',
489 | '🚌',
490 | '🚎',
491 | '🏎',
492 | '🚓',
493 | '🚑',
494 | '🚒',
495 | '🚐',
496 | '🚚',
497 | '🚛',
498 | '🚜',
499 | '🏍',
500 | '🚲',
501 | '🚨',
502 | '🚔',
503 | '🚍',
504 | '🚘',
505 | '🚖',
506 | '🚡',
507 | '🚠',
508 | '🚟',
509 | '🚃',
510 | '🚋',
511 | '🚝',
512 | '🚄',
513 | '🚅',
514 | '🚈',
515 | '🚞',
516 | '🚂',
517 | '🚆',
518 | '🚇',
519 | '🚊',
520 | '🚉',
521 | '🚁',
522 | '🛩',
523 | '✈️',
524 | '🛫',
525 | '🛬',
526 | '⛵️',
527 | '🛥',
528 | '🚤',
529 | '⛴',
530 | '🛳',
531 | '🚀',
532 | '🛰',
533 | '💺',
534 | '⚓️',
535 | '🚧',
536 | '⛽️',
537 | '🚏',
538 | '🚦',
539 | '🚥',
540 | '🏁',
541 | '🚢',
542 | '🎡',
543 | '🎢',
544 | '🎠',
545 | '🏗',
546 | '🌁',
547 | '🗼',
548 | '🏭',
549 | '⛲️',
550 | '🎑',
551 | '⛰',
552 | '🏔',
553 | '🗻',
554 | '🌋',
555 | '🗾',
556 | '🏕',
557 | '⛺️',
558 | '🏞',
559 | '🛣',
560 | '🛤',
561 | '🌅',
562 | '🌄',
563 | '🏜',
564 | '🏖',
565 | '🏝',
566 | '🌇',
567 | '🌆',
568 | '🏙',
569 | '🌃',
570 | '🌉',
571 | '🌌',
572 | '🌠',
573 | '🎇',
574 | '🎆',
575 | '🌈',
576 | '🏘',
577 | '🏰',
578 | '🏯',
579 | '🏟',
580 | '🗽',
581 | '🏠',
582 | '🏡',
583 | '🏚',
584 | '🏢',
585 | '🏬',
586 | '🏣',
587 | '🏤',
588 | '🏥',
589 | '🏦',
590 | '🏨',
591 | '🏪',
592 | '🏫',
593 | '🏩',
594 | '💒',
595 | '🏛',
596 | '⛪️',
597 | '🕌',
598 | '🕍',
599 | '🕋',
600 | '⛩',
601 | ],
602 | objects: [
603 | '⌚️',
604 | '📱',
605 | '📲',
606 | '💻',
607 | '⌨',
608 | '🖥',
609 | '🖨',
610 | '🖱',
611 | '🖲',
612 | '🕹',
613 | '🗜',
614 | '💽',
615 | '💾',
616 | '💿',
617 | '📀',
618 | '📼',
619 | '📷',
620 | '📸',
621 | '📹',
622 | '🎥',
623 | '📽',
624 | '🎞',
625 | '📞',
626 | '☎️',
627 | '📟',
628 | '📠',
629 | '📺',
630 | '📻',
631 | '🎙',
632 | '🎚',
633 | '🎛',
634 | '⏱',
635 | '⏲',
636 | '⏰',
637 | '🕰',
638 | '⏳',
639 | '⌛️',
640 | '📡',
641 | '🔋',
642 | '🔌',
643 | '💡',
644 | '🔦',
645 | '🕯',
646 | '🗑',
647 | '🛢',
648 | '💸',
649 | '💵',
650 | '💴',
651 | '💶',
652 | '💷',
653 | '💰',
654 | '💳',
655 | '💎',
656 | '⚖',
657 | '🔧',
658 | '🔨',
659 | '⚒',
660 | '🛠',
661 | '⛏',
662 | '🔩',
663 | '⚙',
664 | '⛓',
665 | '🔫',
666 | '💣',
667 | '🔪',
668 | '🗡',
669 | '⚔',
670 | '🛡',
671 | '🚬',
672 | '☠',
673 | '⚰',
674 | '⚱',
675 | '🏺',
676 | '🔮',
677 | '📿',
678 | '💈',
679 | '⚗',
680 | '🔭',
681 | '🔬',
682 | '🕳',
683 | '💊',
684 | '💉',
685 | '🌡',
686 | '🏷',
687 | '🔖',
688 | '🚽',
689 | '🚿',
690 | '🛁',
691 | '🔑',
692 | '🗝',
693 | '🛋',
694 | '🛌',
695 | '🛏',
696 | '🚪',
697 | '🛎',
698 | '🖼',
699 | '🗺',
700 | '⛱',
701 | '🗿',
702 | '🛍',
703 | '🎈',
704 | '🎏',
705 | '🎀',
706 | '🎁',
707 | '🎊',
708 | '🎉',
709 | '🎎',
710 | '🎐',
711 | '🎌',
712 | '🏮',
713 | '✉️',
714 | '📩',
715 | '📨',
716 | '📧',
717 | '💌',
718 | '📮',
719 | '📪',
720 | '📫',
721 | '📬',
722 | '📭',
723 | '📦',
724 | '📯',
725 | '📥',
726 | '📤',
727 | '📜',
728 | '📃',
729 | '📑',
730 | '📊',
731 | '📈',
732 | '📉',
733 | '📄',
734 | '📅',
735 | '📆',
736 | '🗓',
737 | '📇',
738 | '🗃',
739 | '🗳',
740 | '🗄',
741 | '📋',
742 | '🗒',
743 | '📁',
744 | '📂',
745 | '🗂',
746 | '🗞',
747 | '📰',
748 | '📓',
749 | '📕',
750 | '📗',
751 | '📘',
752 | '📙',
753 | '📔',
754 | '📒',
755 | '📚',
756 | '📖',
757 | '🔗',
758 | '📎',
759 | '🖇',
760 | '✂️',
761 | '📐',
762 | '📏',
763 | '📌',
764 | '📍',
765 | '🚩',
766 | '🏳',
767 | '🏴',
768 | '🔐',
769 | '🔒',
770 | '🔓',
771 | '🔏',
772 | '🖊',
773 | '🖊',
774 | '🖋',
775 | '✒️',
776 | '📝',
777 | '✏️',
778 | '🖍',
779 | '🖌',
780 | '🔍',
781 | '🔎',
782 | ],
783 | symbols: [
784 | '❤️',
785 | '💛',
786 | '💙',
787 | '💜',
788 | '💔',
789 | '❣️',
790 | '💕',
791 | '💞',
792 | '💓',
793 | '💗',
794 | '💖',
795 | '💘',
796 | '💝',
797 | '💟',
798 | '☮',
799 | '✝️',
800 | '☪',
801 | '🕉',
802 | '☸',
803 | '✡️',
804 | '🔯',
805 | '🕎',
806 | '☯️',
807 | '☦',
808 | '🛐',
809 | '⛎',
810 | '♈️',
811 | '♉️',
812 | '♊️',
813 | '♋️',
814 | '♌️',
815 | '♍️',
816 | '♎️',
817 | '♏️',
818 | '♐️',
819 | '♑️',
820 | '♒️',
821 | '♓️',
822 | '🆔',
823 | '⚛',
824 | '🈳',
825 | '🈹',
826 | '☢',
827 | '☣',
828 | '📴',
829 | '📳',
830 | '🈶',
831 | '🈚️',
832 | '🈸',
833 | '🈺',
834 | '🈷️',
835 | '✴️',
836 | '🆚',
837 | '🉑',
838 | '💮',
839 | '🉐',
840 | '㊙️',
841 | '㊗️',
842 | '🈴',
843 | '🈵',
844 | '🈲',
845 | '🅰️',
846 | '🅱️',
847 | '🆎',
848 | '🆑',
849 | '🅾️',
850 | '🆘',
851 | '⛔️',
852 | '📛',
853 | '🚫',
854 | '❌',
855 | '⭕️',
856 | '💢',
857 | '♨️',
858 | '🚷',
859 | '🚯',
860 | '🚳',
861 | '🚱',
862 | '🔞',
863 | '📵',
864 | '❗️',
865 | '❕',
866 | '❓',
867 | '❔',
868 | '‼️',
869 | '⁉️',
870 | '💯',
871 | '🔅',
872 | '🔆',
873 | '🔱',
874 | '⚜',
875 | '〽️',
876 | '⚠️',
877 | '🚸',
878 | '🔰',
879 | '♻️',
880 | '🈯️',
881 | '💹',
882 | '❇️',
883 | '✳️',
884 | '❎',
885 | '✅',
886 | '💠',
887 | '🌀',
888 | '➿',
889 | '🌐',
890 | 'Ⓜ️',
891 | '🏧',
892 | '🈂️',
893 | '🛂',
894 | '🛃',
895 | '🛄',
896 | '🛅',
897 | '♿️',
898 | '🚭',
899 | '🚾',
900 | '🅿️',
901 | '🚰',
902 | '🚹',
903 | '🚺',
904 | '🚼',
905 | '🚻',
906 | '🚮',
907 | '🎦',
908 | '📶',
909 | '🈁',
910 | '🆖',
911 | '🆗',
912 | '🆙',
913 | '🆒',
914 | '🆕',
915 | '🆓',
916 | '0️⃣',
917 | '1️⃣',
918 | '2️⃣',
919 | '3️⃣',
920 | '4️⃣',
921 | '5️⃣',
922 | '6️⃣',
923 | '7️⃣',
924 | '8️⃣',
925 | '9️⃣',
926 | '🔟',
927 | '🔢',
928 | '▶️',
929 | '⏸',
930 | '⏯',
931 | '⏹',
932 | '⏺',
933 | '⏭',
934 | '⏮',
935 | '⏩',
936 | '⏪',
937 | '🔀',
938 | '🔁',
939 | '🔂',
940 | '◀️',
941 | '🔼',
942 | '🔽',
943 | '⏫',
944 | '⏬',
945 | '➡️',
946 | '⬅️',
947 | '⬆️',
948 | '⬇️',
949 | '↗️',
950 | '↘️',
951 | '↙️',
952 | '↖️',
953 | '↕️',
954 | '↔️',
955 | '🔄',
956 | '↪️',
957 | '↩️',
958 | '⤴️',
959 | '⤵️',
960 | '#️⃣',
961 | '*️⃣',
962 | 'ℹ️',
963 | '🔤',
964 | '🔡',
965 | '🔠',
966 | '🔣',
967 | '🎵',
968 | '🎶',
969 | '〰️',
970 | '➰',
971 | '✔️',
972 | '🔃',
973 | '➕',
974 | '➖',
975 | '➗',
976 | '✖️',
977 | '💲',
978 | '💱',
979 | '©️',
980 | '®️',
981 | '™️',
982 | '🔚',
983 | '🔙',
984 | '🔛',
985 | '🔝',
986 | '🔜',
987 | '☑️',
988 | '🔘',
989 | '⚪️',
990 | '⚫️',
991 | '🔴',
992 | '🔵',
993 | '🔸',
994 | '🔹',
995 | '🔶',
996 | '🔷',
997 | '🔺',
998 | '▪️',
999 | '▫️',
1000 | '⬛️',
1001 | '⬜️',
1002 | '🔻',
1003 | '◼️',
1004 | '◻️',
1005 | '◾️',
1006 | '◽️',
1007 | '🔲',
1008 | '🔳',
1009 | '🔈',
1010 | '🔉',
1011 | '🔊',
1012 | '🔇',
1013 | '📣',
1014 | '📢',
1015 | '🔔',
1016 | '🔕',
1017 | '🃏',
1018 | '🀄️',
1019 | '♠️',
1020 | '♣️',
1021 | '♥️',
1022 | '♦️',
1023 | '🎴',
1024 | '👁🗨',
1025 | '💭',
1026 | '🗯',
1027 | '💬',
1028 | '🕐',
1029 | '🕑',
1030 | '🕒',
1031 | '🕓',
1032 | '🕔',
1033 | '🕕',
1034 | '🕖',
1035 | '🕗',
1036 | '🕘',
1037 | '🕙',
1038 | '🕚',
1039 | '🕛',
1040 | '🕜',
1041 | '🕝',
1042 | '🕞',
1043 | '🕟',
1044 | '🕠',
1045 | '🕡',
1046 | '🕢',
1047 | '🕣',
1048 | '🕤',
1049 | '🕥',
1050 | '🕦',
1051 | '🕧',
1052 | ],
1053 | flags: [
1054 | '🇦🇫',
1055 | '🇦🇽',
1056 | '🇦🇱',
1057 | '🇩🇿',
1058 | '🇦🇸',
1059 | '🇦🇩',
1060 | '🇦🇴',
1061 | '🇦🇮',
1062 | '🇦🇶',
1063 | '🇦🇬',
1064 | '🇦🇷',
1065 | '🇦🇲',
1066 | '🇦🇼',
1067 | '🇦🇺',
1068 | '🇦🇹',
1069 | '🇦🇿',
1070 | '🇧🇸',
1071 | '🇧🇭',
1072 | '🇧🇩',
1073 | '🇧🇧',
1074 | '🇧🇾',
1075 | '🇧🇪',
1076 | '🇧🇿',
1077 | '🇧🇯',
1078 | '🇧🇲',
1079 | '🇧🇹',
1080 | '🇧🇴',
1081 | '🇧🇶',
1082 | '🇧🇦',
1083 | '🇧🇼',
1084 | '🇧🇷',
1085 | '🇮🇴',
1086 | '🇻🇬',
1087 | '🇧🇳',
1088 | '🇧🇬',
1089 | '🇧🇫',
1090 | '🇧🇮',
1091 | '🇨🇻',
1092 | '🇰🇭',
1093 | '🇨🇲',
1094 | '🇨🇦',
1095 | '🇮🇨',
1096 | '🇰🇾',
1097 | '🇨🇫',
1098 | '🇹🇩',
1099 | '🇨🇱',
1100 | '🇨🇳',
1101 | '🇨🇽',
1102 | '🇨🇨',
1103 | '🇨🇴',
1104 | '🇰🇲',
1105 | '🇨🇬',
1106 | '🇨🇩',
1107 | '🇨🇰',
1108 | '🇨🇷',
1109 | '🇭🇷',
1110 | '🇨🇺',
1111 | '🇨🇼',
1112 | '🇨🇾',
1113 | '🇨🇿',
1114 | '🇩🇰',
1115 | '🇩🇯',
1116 | '🇩🇲',
1117 | '🇩🇴',
1118 | '🇪🇨',
1119 | '🇪🇬',
1120 | '🇸🇻',
1121 | '🇬🇶',
1122 | '🇪🇷',
1123 | '🇪🇪',
1124 | '🇪🇹',
1125 | '🇪🇺',
1126 | '🇫🇰',
1127 | '🇫🇴',
1128 | '🇫🇯',
1129 | '🇫🇮',
1130 | '🇫🇷',
1131 | '🇬🇫',
1132 | '🇵🇫',
1133 | '🇹🇫',
1134 | '🇬🇦',
1135 | '🇬🇲',
1136 | '🇬🇪',
1137 | '🇩🇪',
1138 | '🇬🇭',
1139 | '🇬🇮',
1140 | '🇬🇷',
1141 | '🇬🇱',
1142 | '🇬🇩',
1143 | '🇬🇵',
1144 | '🇬🇺',
1145 | '🇬🇹',
1146 | '🇬🇬',
1147 | '🇬🇳',
1148 | '🇬🇼',
1149 | '🇬🇾',
1150 | '🇭🇹',
1151 | '🇭🇳',
1152 | '🇭🇰',
1153 | '🇭🇺',
1154 | '🇮🇸',
1155 | '🇮🇳',
1156 | '🇮🇩',
1157 | '🇮🇷',
1158 | '🇮🇶',
1159 | '🇮🇪',
1160 | '🇮🇲',
1161 | '🇮🇱',
1162 | '🇮🇹',
1163 | '🇨🇮',
1164 | '🇯🇲',
1165 | '🇯🇵',
1166 | '🇯🇪',
1167 | '🇯🇴',
1168 | '🇰🇿',
1169 | '🇰🇪',
1170 | '🇰🇮',
1171 | '🇽🇰',
1172 | '🇰🇼',
1173 | '🇰🇬',
1174 | '🇱🇦',
1175 | '🇱🇻',
1176 | '🇱🇧',
1177 | '🇱🇸',
1178 | '🇱🇷',
1179 | '🇱🇾',
1180 | '🇱🇮',
1181 | '🇱🇹',
1182 | '🇱🇺',
1183 | '🇲🇴',
1184 | '🇲🇰',
1185 | '🇲🇬',
1186 | '🇲🇼',
1187 | '🇲🇾',
1188 | '🇲🇻',
1189 | '🇲🇱',
1190 | '🇲🇹',
1191 | '🇲🇭',
1192 | '🇲🇶',
1193 | '🇲🇷',
1194 | '🇲🇺',
1195 | '🇾🇹',
1196 | '🇲🇽',
1197 | '🇫🇲',
1198 | '🇲🇩',
1199 | '🇲🇨',
1200 | '🇲🇳',
1201 | '🇲🇪',
1202 | '🇲🇸',
1203 | '🇲🇦',
1204 | '🇲🇿',
1205 | '🇲🇲',
1206 | '🇳🇦',
1207 | '🇳🇷',
1208 | '🇳🇵',
1209 | '🇳🇱',
1210 | '🇳🇨',
1211 | '🇳🇿',
1212 | '🇳🇮',
1213 | '🇳🇪',
1214 | '🇳🇬',
1215 | '🇳🇺',
1216 | '🇳🇫',
1217 | '🇲🇵',
1218 | '🇰🇵',
1219 | '🇳🇴',
1220 | '🇴🇲',
1221 | '🇵🇰',
1222 | '🇵🇼',
1223 | '🇵🇸',
1224 | '🇵🇦',
1225 | '🇵🇬',
1226 | '🇵🇾',
1227 | '🇵🇪',
1228 | '🇵🇭',
1229 | '🇵🇳',
1230 | '🇵🇱',
1231 | '🇵🇹',
1232 | '🇵🇷',
1233 | '🇶🇦',
1234 | '🇷🇪',
1235 | '🇷🇴',
1236 | '🇷🇺',
1237 | '🇷🇼',
1238 | '🇧🇱',
1239 | '🇸🇭',
1240 | '🇰🇳',
1241 | '🇱🇨',
1242 | '🇵🇲',
1243 | '🇻🇨',
1244 | '🇼🇸',
1245 | '🇸🇲',
1246 | '🇸🇹',
1247 | '🇸🇦',
1248 | '🇸🇳',
1249 | '🇷🇸',
1250 | '🇸🇨',
1251 | '🇸🇱',
1252 | '🇸🇬',
1253 | '🇸🇽',
1254 | '🇸🇰',
1255 | '🇸🇮',
1256 | '🇸🇧',
1257 | '🇸🇴',
1258 | '🇿🇦',
1259 | '🇬🇸',
1260 | '🇰🇷',
1261 | '🇸🇸',
1262 | '🇪🇸',
1263 | '🇱🇰',
1264 | '🇸🇩',
1265 | '🇸🇷',
1266 | '🇸🇿',
1267 | '🇸🇪',
1268 | '🇨🇭',
1269 | '🇸🇾',
1270 | '🇹🇼',
1271 | '🇹🇯',
1272 | '🇹🇿',
1273 | '🇹🇭',
1274 | '🇹🇱',
1275 | '🇹🇬',
1276 | '🇹🇰',
1277 | '🇹🇴',
1278 | '🇹🇹',
1279 | '🇹🇳',
1280 | '🇹🇷',
1281 | '🇹🇲',
1282 | '🇹🇨',
1283 | '🇹🇻',
1284 | '🇺🇬',
1285 | '🇺🇦',
1286 | '🇦🇪',
1287 | '🇬🇧',
1288 | '🇺🇸',
1289 | '🇻🇮',
1290 | '🇺🇾',
1291 | '🇺🇿',
1292 | '🇻🇺',
1293 | '🇻🇦',
1294 | '🇻🇪',
1295 | '🇻🇳',
1296 | '🇼🇫',
1297 | '🇪🇭',
1298 | '🇾🇪',
1299 | '🇿🇲',
1300 | '🇿🇼',
1301 | ],
1302 | };
1303 |
1304 | export default emoji;
1305 |
--------------------------------------------------------------------------------
/src/helpers/groupBy.ts:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/a/34890276/9931154
2 | // https://stackoverflow.com/a/61400956/9931154
3 | export const groupBy = (
4 | xs: T[] = [],
5 | key: U
6 | ): { [key: string]: T[] } => {
7 | return xs.reduce((rv: any, x: T) => {
8 | (rv[x[key]] = rv[x[key]] || []).push(x);
9 | return rv;
10 | }, {});
11 | };
12 |
--------------------------------------------------------------------------------
/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export { default as emoji } from './emoji';
2 | export * from './groupBy';
3 | export { default as Hover } from './Hover';
4 | export * from './HoverStyle';
5 | export { default as icons } from './icons';
6 | export * from './interfaces';
7 | export * from './slack';
8 | export * from './strings';
9 | export * from './types';
10 | export * from './useHover';
11 | export { default as withActive } from './withActive';
12 |
--------------------------------------------------------------------------------
/src/helpers/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface CounterObject {
2 | emoji: string;
3 | by: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/helpers/slack.ts:
--------------------------------------------------------------------------------
1 | export const emojiColors = [
2 | '#b7e887',
3 | '#b5e0fe',
4 | '#f9ef67',
5 | '#f3c1fd',
6 | '#ffe1ae',
7 | '#e0dfff',
8 | ];
9 |
10 | export const sectionSlugToName = (name: string): string => {
11 | const thing: { [key: string]: string } = {
12 | mine: 'Frequently Used',
13 | people: 'People',
14 | nature: 'Nature',
15 | 'food-and-drink': 'Food & Drink',
16 | activity: 'Activity',
17 | 'travel-and-places': 'Travel & Places',
18 | objects: 'Objects',
19 | symbols: 'Symbols',
20 | flags: 'Flags',
21 | };
22 | return thing[name];
23 | };
24 |
--------------------------------------------------------------------------------
/src/helpers/strings.ts:
--------------------------------------------------------------------------------
1 | export function listOfNames(names: string[]) {
2 | return [names.slice(0, -1).join(', '), names.slice(-1)[0]].join(
3 | names.length < 2 ? '' : ' and '
4 | );
5 | }
6 |
--------------------------------------------------------------------------------
/src/helpers/types.ts:
--------------------------------------------------------------------------------
1 | export interface Reaction {
2 | node: React.ReactNode;
3 | label: string;
4 | key?: string;
5 | }
6 |
7 | export interface ReactionCounterObject {
8 | node: JSX.Element;
9 | label: string;
10 | by: string;
11 | }
12 |
--------------------------------------------------------------------------------
/src/helpers/useHover.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const HoverContext = React.createContext(false);
4 |
5 | export const useHover = () => React.useContext(HoverContext);
6 |
--------------------------------------------------------------------------------
/src/helpers/withActive.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // HOC that keeps track of weather or not the component is being clicked.
4 | // https://stackoverflow.com/a/53453431/9931154
5 | export const withActive = >(
6 | Component: React.ElementType
7 | ) => {
8 | return React.forwardRef((props, ref) => {
9 | const [active, setActive] = React.useState(false);
10 |
11 | const handleMouseDown = React.useCallback(() => {
12 | setActive(true);
13 | }, []);
14 | const handleMouseUp = React.useCallback(() => {
15 | setActive(false);
16 | }, []);
17 |
18 | return (
19 |
20 |
21 |
22 | );
23 | });
24 | };
25 |
26 | export default withActive;
27 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export default 'Reactions';
2 |
3 | export * from './components';
4 | export * from './helpers';
5 |
--------------------------------------------------------------------------------
/stories/FacebookCounter.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | defaultProps,
5 | FacebookCounter,
6 | FacebookCounterProps,
7 | } from '../src/components/facebook/FacebookCounter';
8 |
9 | const meta: Meta = {
10 | title: 'FacebookCounter',
11 | component: FacebookCounter,
12 | argTypes: {
13 | children: {
14 | control: {
15 | type: 'text',
16 | },
17 | },
18 | },
19 | parameters: {
20 | controls: { expanded: true },
21 | },
22 | };
23 |
24 | export default meta;
25 |
26 | const Template: Story = args => (
27 |
28 | );
29 |
30 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
31 | // https://storybook.js.org/docs/react/workflows/unit-testing
32 | export const Default = Template.bind({});
33 |
34 | Default.args = { ...defaultProps };
35 |
--------------------------------------------------------------------------------
/stories/FacebookCounterReaction.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | FacebookCounterReaction,
5 | FacebookCounterReactionProps,
6 | } from '../src/components/facebook/FacebookCounterReaction';
7 |
8 | const meta: Meta = {
9 | title: 'FacebookCounterReaction',
10 | component: FacebookCounterReaction,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {
34 | reaction: 'angry',
35 | bg: '#000',
36 | variant: 'facebook',
37 | };
38 |
--------------------------------------------------------------------------------
/stories/FacebookSelector.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | defaultProps,
5 | FacebookSelector,
6 | FacebookSelectorProps,
7 | } from '../src/components/facebook/FacebookSelector';
8 | import './helper.css';
9 |
10 | const meta: Meta = {
11 | title: 'FacebookSelector',
12 | component: FacebookSelector,
13 | argTypes: {
14 | children: {
15 | control: {
16 | type: 'text',
17 | },
18 | },
19 | },
20 | parameters: {
21 | controls: { expanded: true },
22 | },
23 | };
24 |
25 | export default meta;
26 |
27 | const Template: Story = args => (
28 |
29 |
30 |
31 | );
32 |
33 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
34 | // https://storybook.js.org/docs/react/workflows/unit-testing
35 | export const Default = Template.bind({});
36 |
37 | Default.args = { ...defaultProps };
38 |
--------------------------------------------------------------------------------
/stories/FacebookSelectorEmoji.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | FacebookSelectorEmoji,
5 | FacebookSelectorEmojiProps,
6 | } from '../src/components/facebook/FacebookSelectorEmoji';
7 | import { icons } from '../src/helpers';
8 |
9 | const meta: Meta = {
10 | title: 'FacebookSelectorEmoji',
11 | component: FacebookSelectorEmoji,
12 | argTypes: {
13 | children: {
14 | control: {
15 | type: 'text',
16 | },
17 | },
18 | },
19 | parameters: {
20 | controls: { expanded: true },
21 | },
22 | };
23 |
24 | export default meta;
25 |
26 | const Template: Story = args => {
27 | const ref = React.useRef(null);
28 | return (
29 |
30 | {
31 | args.onSelect(args.label);
32 | console.log(ref.current);
33 | }}/>
34 |
35 | );
36 | };
37 |
38 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
39 | // https://storybook.js.org/docs/react/workflows/unit-testing
40 | export const Default = Template.bind({});
41 |
42 | Default.args = {
43 | icon: icons.find('facebook', 'haha'),
44 | label: 'cool',
45 | onSelect: (label: string) => console.log(label),
46 | };
47 |
--------------------------------------------------------------------------------
/stories/GithubCounter.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | defaultProps,
5 | GithubCounter,
6 | GithubCounterProps,
7 | } from '../src/components/github/GithubCounter';
8 |
9 | const meta: Meta = {
10 | title: 'GithubCounter',
11 | component: GithubCounter,
12 | argTypes: {
13 | children: {
14 | control: {
15 | type: 'text',
16 | },
17 | },
18 | },
19 | parameters: {
20 | controls: { expanded: true },
21 | },
22 | };
23 |
24 | export default meta;
25 |
26 | const Template: Story = args => ;
27 |
28 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
29 | // https://storybook.js.org/docs/react/workflows/unit-testing
30 | export const Default = Template.bind({});
31 |
32 | Default.args = { ...defaultProps };
33 |
--------------------------------------------------------------------------------
/stories/GithubCounterGroup.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | GithubCounterGroup,
5 | GithubCounterGroupProps,
6 | } from '../src/components/github/GithubCounterGroup';
7 |
8 | const meta: Meta = {
9 | title: 'GithubCounterGroup',
10 | component: GithubCounterGroup,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {
34 | emoji: '🎉',
35 | count: '3',
36 | names: ['Jon', 'Jo', 'Jeff'],
37 | active: true,
38 | };
39 |
--------------------------------------------------------------------------------
/stories/GithubSelector.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | defaultProps,
5 | GithubSelector,
6 | GithubSelectorProps,
7 | } from '../src/components/github/GithubSelector';
8 |
9 | const meta: Meta = {
10 | title: 'GithubSelector',
11 | component: GithubSelector,
12 | argTypes: {
13 | children: {
14 | control: {
15 | type: 'text',
16 | },
17 | },
18 | },
19 | parameters: {
20 | controls: { expanded: true },
21 | },
22 | };
23 |
24 | export default meta;
25 |
26 | const Template: Story = args => (
27 |
28 | );
29 |
30 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
31 | // https://storybook.js.org/docs/react/workflows/unit-testing
32 | export const Default = Template.bind({});
33 |
34 | Default.args = { ...defaultProps };
35 |
--------------------------------------------------------------------------------
/stories/GithubSelectorEmoji.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | GithubSelectorEmoji,
5 | GithubSelectorEmojiProps,
6 | } from '../src/components/github/GithubSelectorEmoji';
7 |
8 | const meta: Meta = {
9 | title: 'GithubSelectorEmoji',
10 | component: GithubSelectorEmoji,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {
34 | shortCode: '😄',
35 | active: true,
36 | };
37 |
--------------------------------------------------------------------------------
/stories/PokemonCounter.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | PokemonCounter,
5 | PokemonCounterProps,
6 | } from '../src/components/pokemon/PokemonCounter';
7 |
8 | const meta: Meta = {
9 | title: 'PokemonCounter',
10 | component: PokemonCounter,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {};
34 |
--------------------------------------------------------------------------------
/stories/PokemonSelector.css:
--------------------------------------------------------------------------------
1 | .animation-story {
2 | width: 100vw;
3 | height: 100vh;
4 | font-family: sans-serif;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | align-items: center;
9 | }
10 |
11 | .PokemonSelector_Idle {
12 | -webkit-animation: slide-out 0.5s ease-in-out;
13 | animation: slide-out 0.5s ease-in-out;
14 | -webkit-transform: translateX(100%);
15 | transform: translateX(100%);
16 | opacity: 0;
17 | pointer-events: none;
18 | }
19 |
20 | .PokemonSelector_Active {
21 | -webkit-animation: slide-in 0.5s ease-in-out;
22 | animation: slide-in 0.5s ease-in-out;
23 | }
24 |
25 | @-webkit-keyframes slide-in {
26 | 0% {
27 | opacity: 0;
28 | -webkit-transform: translateX(100%);
29 | }
30 | 100% {
31 | opacity: 1;
32 | -webkit-transform: translateX(0);
33 | }
34 | }
35 |
36 | @keyframes slide-in {
37 | 0% {
38 | opacity: 0;
39 | -webkit-transform: translateX(100%);
40 | }
41 | 100% {
42 | opacity: 1;
43 | -webkit-transform: translateX(0);
44 | }
45 | }
46 |
47 | @-webkit-keyframes slide-out {
48 | 0% {
49 | opacity: 1;
50 | -webkit-transform: translateX(0);
51 | }
52 | 100% {
53 | opacity: 0;
54 | -webkit-transform: translateX(100%);
55 | }
56 | }
57 |
58 | @keyframes slide-out {
59 | 0% {
60 | opacity: 1;
61 | -webkit-transform: translateX(0);
62 | }
63 | 100% {
64 | opacity: 0;
65 | -webkit-transform: translateX(100%);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/stories/PokemonSelector.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import { defaultProps } from '../src/components/facebook/FacebookSelector';
4 | import {
5 | PokemonSelector,
6 | PokemonSelectorProps,
7 | } from '../src/components/pokemon/PokemonSelector';
8 | import './PokemonSelector.css';
9 |
10 | const meta: Meta = {
11 | title: 'PokemonSelector',
12 | component: PokemonSelector,
13 | argTypes: {
14 | children: {
15 | control: {
16 | type: 'text',
17 | },
18 | },
19 | },
20 | parameters: {
21 | controls: { expanded: true },
22 | },
23 | };
24 |
25 | export default meta;
26 |
27 | const Template: Story = args => (
28 |
29 | );
30 |
31 | const Animation: Story = args => {
32 | const [emojiSelector, setEmojiSelector] = React.useState(false);
33 | return (
34 |
35 |
44 |
45 |
56 |
57 | );
58 | };
59 |
60 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
61 | // https://storybook.js.org/docs/react/workflows/unit-testing
62 | export const Default = Template.bind({});
63 | export const SlideInAnimation = Animation.bind({});
64 |
65 | const { variant, ...props } = defaultProps;
66 | Default.args = { ...props };
67 |
68 | SlideInAnimation.args = { ...props };
69 |
--------------------------------------------------------------------------------
/stories/ReactionBarSelector.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | defaultProps,
5 | ReactionBarSelector,
6 | ReactionBarSelectorProps,
7 | } from '../src/components/custom/ReactionBarSelector';
8 | import './helper.css';
9 |
10 | const meta: Meta = {
11 | title: 'ReactionBarSelector',
12 | component: ReactionBarSelector,
13 | argTypes: {
14 | children: {
15 | control: {
16 | type: 'text',
17 | },
18 | },
19 | },
20 | parameters: {
21 | controls: { expanded: true },
22 | },
23 | };
24 |
25 | export default meta;
26 |
27 | const Template: Story = args => (
28 |
29 |
30 |
31 | );
32 |
33 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
34 | // https://storybook.js.org/docs/react/workflows/unit-testing
35 | export const Default = Template.bind({});
36 | export const Images = Template.bind({});
37 |
38 | Default.args = { ...defaultProps };
39 |
40 | Images.args = {
41 | reactions: [
42 | {
43 | key: 'happy',
44 | label: 'haha',
45 | node: (
46 |
50 | ),
51 | },
52 | {
53 | key: 'sad',
54 | label: 'cry',
55 | node: (
56 |
60 | ),
61 | },
62 | {
63 | key: 'love',
64 | label: 'heart',
65 | node: (
66 |
70 | ),
71 | },
72 | {
73 | key: 'surprise',
74 | label: 'wow',
75 | node: (
76 |
80 | ),
81 | },
82 | ],
83 | iconSize: 40,
84 | };
85 |
--------------------------------------------------------------------------------
/stories/ReactionBarSelectorEmoji.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | ReactionBarSelectorEmoji,
5 | ReactionBarSelectorEmojiProps,
6 | } from '../src/components/custom/ReactionBarSelectorEmoji';
7 | import './helper.css';
8 |
9 | const meta: Meta = {
10 | title: 'ReactionBarSelectorEmoji',
11 | component: ReactionBarSelectorEmoji,
12 | argTypes: {
13 | children: {
14 | control: {
15 | type: 'text',
16 | },
17 | },
18 | },
19 | parameters: {
20 | controls: { expanded: true },
21 | },
22 | };
23 |
24 | export default meta;
25 |
26 | const Template: Story = args => {
27 | return (
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
35 | // https://storybook.js.org/docs/react/workflows/unit-testing
36 | export const Default = Template.bind({});
37 |
38 | Default.args = {
39 | reaction: {
40 | node: 😆
,
41 | label: 'haha',
42 | },
43 | onSelect: (label: string) => console.log(label),
44 | };
45 |
--------------------------------------------------------------------------------
/stories/ReactionCounter.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | ReactionCounter,
5 | ReactionCounterProps,
6 | } from '../src/components/custom/ReactionCounter';
7 | import './helper.css';
8 |
9 | const meta: Meta = {
10 | title: 'ReactionCounter',
11 | component: ReactionCounter,
12 | argTypes: {
13 | children: {
14 | control: {
15 | type: 'text',
16 | },
17 | },
18 | },
19 | parameters: {
20 | controls: { expanded: true },
21 | },
22 | };
23 |
24 | export default meta;
25 |
26 | const Template: Story = args => (
27 |
28 | );
29 |
30 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
31 | // https://storybook.js.org/docs/react/workflows/unit-testing
32 | export const Default = Template.bind({});
33 |
34 | Default.args = {
35 | user: 'Elliot',
36 | reactions: [
37 | {
38 | by: 'Case Sandberg',
39 | label: 'haha',
40 | node: (
41 |
44 | ),
45 | },
46 | {
47 | by: 'Charlie',
48 | label: 'haha',
49 | node: (
50 |
53 | ),
54 | },
55 | {
56 | by: 'Elliot',
57 | label: 'heart',
58 | node: (
59 |
62 | ),
63 | },
64 | ]
65 | };
66 |
--------------------------------------------------------------------------------
/stories/ReactionCounterBackground.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | ReactionCounter,
5 | ReactionCounterProps,
6 | } from '../src/components/custom/ReactionCounter';
7 | import './helper.css';
8 |
9 | const meta: Meta = {
10 | title: 'ReactionCounter',
11 | component: ReactionCounter,
12 | argTypes: {
13 | children: {
14 | control: {
15 | type: 'text',
16 | },
17 | },
18 | },
19 | parameters: {
20 | controls: { expanded: true },
21 | },
22 | };
23 |
24 | export default meta;
25 |
26 | const Template: Story = (args) => (
27 |
28 | );
29 |
30 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
31 | // https://storybook.js.org/docs/react/workflows/unit-testing
32 | export const Default = Template.bind({});
33 |
34 | const reactions = [
35 | {
36 | label: 'like',
37 | node: (
38 |
39 | 👍🏾
40 |
41 | ),
42 | by: 'name',
43 | },
44 | {
45 | label: 'love',
46 | node: (
47 | ❤️
48 | ),
49 | by: 'vens',
50 | },
51 | {
52 | label: 'care',
53 | node: (
54 | 🥰
55 | ),
56 | by: 'jo',
57 | },
58 | {
59 | label: 'haha',
60 | node: (
61 | 😆
62 | ),
63 | by: 'abla',
64 | },
65 | {
66 | label: 'wow',
67 | node: (
68 | 😲
69 | ),
70 | by: 'rosa',
71 | },
72 | {
73 | label: 'sad',
74 | node: (
75 | 😔
76 | ),
77 | by: 'peter',
78 | },
79 | {
80 | label: 'angry',
81 | node: (
82 | 😡
83 | ),
84 | by: 'jove',
85 | },
86 | { label: 'angry', node: 😡
, by: 'john' },
87 | { label: 'angry', node: 😡
, by: 'jane' },
88 | { label: 'angry', node: 😡
, by: 'scott' },
89 | { label: 'angry', node: 😡
, by: 'dave' },
90 | ];
91 |
92 | Default.args = {
93 | user: 'Elliot',
94 | reactions,
95 | bg: 'transparent',
96 | style: { display: 'flex', flexDirection: 'row', gap: 8 },
97 | };
98 |
--------------------------------------------------------------------------------
/stories/ReactionCounterEmoji.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | ReactionCounterEmoji,
5 | ReactionCounterEmojiProps,
6 | } from '../src/components/custom/ReactionCounterEmoji';
7 | import './helper.css';
8 |
9 | const meta: Meta = {
10 | title: 'ReactionCounterEmoji',
11 | component: ReactionCounterEmoji,
12 | argTypes: {
13 | children: {
14 | control: {
15 | type: 'text',
16 | },
17 | },
18 | },
19 | parameters: {
20 | controls: { expanded: true },
21 | },
22 | };
23 |
24 | export default meta;
25 |
26 | const Template: Story = args => (
27 |
28 | );
29 |
30 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
31 | // https://storybook.js.org/docs/react/workflows/unit-testing
32 | export const Default = Template.bind({});
33 |
34 | Default.args = {
35 | node: (
36 |
39 | ),
40 | bg: 'pink',
41 | iconSize: 24,
42 | };
43 |
--------------------------------------------------------------------------------
/stories/SlackCounter.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | defaultProps,
5 | SlackCounter,
6 | SlackCounterProps,
7 | } from '../src/components/slack/SlackCounter';
8 |
9 | const meta: Meta = {
10 | title: 'SlackCounter',
11 | component: SlackCounter,
12 | argTypes: {
13 | children: {
14 | control: {
15 | type: 'text',
16 | },
17 | },
18 | },
19 | parameters: {
20 | controls: { expanded: true },
21 | },
22 | };
23 |
24 | export default meta;
25 |
26 | const Template: Story = args => ;
27 |
28 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
29 | // https://storybook.js.org/docs/react/workflows/unit-testing
30 | export const Default = Template.bind({});
31 |
32 | Default.args = { ...defaultProps };
33 |
--------------------------------------------------------------------------------
/stories/SlackCounterGroup.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | SlackCounterGroup,
5 | SlackCounterGroupProps,
6 | } from '../src/components/slack/SlackCounterGroup';
7 |
8 | const meta: Meta = {
9 | title: 'SlackCounterGroup',
10 | component: SlackCounterGroup,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {
34 | emoji: '🎉',
35 | count: '3',
36 | names: ['Jon', 'Jo', 'Jeff'],
37 | active: true,
38 | };
39 |
--------------------------------------------------------------------------------
/stories/SlackExample.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import { useState } from 'react';
3 | import { SlackCounter } from '../src/components/slack/SlackCounter';
4 | import { SlackSelector } from '../src/components/slack/SlackSelector';
5 |
6 | const meta: Meta = {
7 | title: 'SlackSelectorExample',
8 | component: SlackSelector,
9 | argTypes: {
10 | children: {
11 | control: {
12 | type: 'text',
13 | },
14 | },
15 | },
16 | parameters: {
17 | controls: { expanded: true },
18 | },
19 | };
20 |
21 | export default meta;
22 |
23 | const Template: Story = () => ;
24 |
25 | export function Reactions() {
26 | const [emojiState, setEmojiState] = useState({
27 | counters: [{ emoji: '🗿', by: 'case' }],
28 | user: 'case',
29 | showSelector: false,
30 | });
31 |
32 | const handleAdd = () => {
33 | setEmojiState({ ...emojiState, showSelector: !emojiState.showSelector });
34 | };
35 |
36 | const handleSelect = async (emoji) => {
37 | const index = emojiState.counters.findIndex(
38 | (x) => x.emoji === emoji && x.by === emojiState.user
39 | );
40 | if (index > -1) {
41 | setEmojiState({
42 | ...emojiState,
43 | counters: [
44 | ...emojiState.counters.slice(0, index),
45 | ...emojiState.counters.slice(index + 1),
46 | ],
47 | showSelector: false,
48 | });
49 | } else {
50 | setEmojiState({
51 | ...emojiState,
52 | counters: [...emojiState.counters, { emoji, by: emojiState.user }],
53 | showSelector: false,
54 | });
55 | }
56 | };
57 |
58 | return (
59 |
60 |
65 | temp
66 |
67 |
73 | {emojiState.showSelector ? (
74 |
82 |
83 |
84 | ) : null}
85 |
86 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/stories/SlackSelector.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | defaultProps,
5 | SlackSelector,
6 | SlackSelectorProps,
7 | } from '../src/components/slack/SlackSelector';
8 |
9 | const meta: Meta = {
10 | title: 'SlackSelector',
11 | component: SlackSelector,
12 | argTypes: {
13 | children: {
14 | control: {
15 | type: 'text',
16 | },
17 | },
18 | },
19 | parameters: {
20 | controls: { expanded: true },
21 | },
22 | };
23 |
24 | export default meta;
25 |
26 | const Template: Story = args => ;
27 |
28 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
29 | // https://storybook.js.org/docs/react/workflows/unit-testing
30 | export const Default = Template.bind({});
31 |
32 | Default.args = { ...defaultProps };
33 |
--------------------------------------------------------------------------------
/stories/SlackSelectorFooter.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | SlackSelectorFooter,
5 | SlackSelectorFooterProps,
6 | } from '../src/components/slack/SlackSelectorFooter';
7 |
8 | const meta: Meta = {
9 | title: 'SlackSelectorFooter',
10 | component: SlackSelectorFooter,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {};
34 |
--------------------------------------------------------------------------------
/stories/SlackSelectorHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | SlackSelectorHeader,
5 | SlackSelectorHeaderProps,
6 | } from '../src/components/slack/SlackSelectorHeader';
7 |
8 | const meta: Meta = {
9 | title: 'SlackSelectorHeader',
10 | component: SlackSelectorHeader,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {
34 | active: 'mine',
35 | };
36 |
--------------------------------------------------------------------------------
/stories/SlackSelectorHeaderTab.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | SlackSelectorHeaderTab,
5 | SlackSelectorHeaderTabProps,
6 | } from '../src/components/slack/SlackSelectorHeaderTab';
7 |
8 | const meta: Meta = {
9 | title: 'SlackSelectorHeaderTab',
10 | component: SlackSelectorHeaderTab,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | const tabs = [
34 | {
35 | icon: '🥰',
36 | id: 'mine',
37 | },
38 | {
39 | icon: '✨',
40 | id: 'people',
41 | },
42 | ];
43 |
44 | Default.args = {
45 | icon: tabs[0].icon,
46 | id: tabs[0].id,
47 | active: true,
48 | };
49 |
--------------------------------------------------------------------------------
/stories/SlackSelectorItems.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | SlackSelectorItems,
5 | SlackSelectorItemsProps,
6 | } from '../src/components/slack/SlackSelectorItems';
7 |
8 | const meta: Meta = {
9 | title: 'SlackSelectorItems',
10 | component: SlackSelectorItems,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {
34 | scrollHeight: '270px',
35 | removeEmojis: [
36 | '🙂',
37 | '🙃',
38 | '☺️',
39 | '🤑',
40 | '🤓',
41 | '🤗',
42 | '🙄',
43 | '🤔',
44 | '🙁',
45 | '☹️',
46 | '🤐',
47 | '🤒',
48 | '🤕',
49 | '🤖',
50 | ],
51 | frequent: [
52 | '👍',
53 | '🐉',
54 | '🙌',
55 | '🗿',
56 | '😊',
57 | '🐬',
58 | '😹',
59 | '👻',
60 | '🚀',
61 | '🚁',
62 | '🏇',
63 | '🇨🇦',
64 | ],
65 | };
66 |
--------------------------------------------------------------------------------
/stories/SlackSelectorSection.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | SlackSelectorSection,
5 | SlackSelectorSectionProps,
6 | } from '../src/components/slack/SlackSelectorSection';
7 |
8 | const meta: Meta = {
9 | title: 'SlackSelectorSection',
10 | component: SlackSelectorSection,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {
34 | slug: '✨',
35 | emojis: ['✨', '🔥'],
36 | };
37 |
--------------------------------------------------------------------------------
/stories/SlackSelectorSectionEmoji.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | SlackSelectorSectionEmoji,
5 | SlackSelectorSectionEmojiProps,
6 | } from '../src/components/slack/SlackSelectorSectionEmoji';
7 |
8 | const meta: Meta = {
9 | title: 'SlackSelectorSectionEmoji',
10 | component: SlackSelectorSectionEmoji,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {
34 | hoverColor: 'blue',
35 | emoji: '🥰',
36 | };
37 |
--------------------------------------------------------------------------------
/stories/YoutubeCounter.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | YoutubeCounter,
5 | YoutubeCounterProps,
6 | } from '../src/components/youtube/YoutubeCounter';
7 |
8 | const meta: Meta = {
9 | title: 'YoutubeCounter',
10 | component: YoutubeCounter,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {
34 | like: '3',
35 | dislike: '2',
36 | };
37 |
--------------------------------------------------------------------------------
/stories/YoutubeCounterButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import {
4 | YoutubeCounterButton,
5 | YoutubeCounterButtonProps,
6 | } from '../src/components/youtube/YoutubeCounterButton';
7 |
8 | const meta: Meta = {
9 | title: 'YoutubeCounterButton',
10 | component: YoutubeCounterButton,
11 | argTypes: {
12 | children: {
13 | control: {
14 | type: 'text',
15 | },
16 | },
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | };
22 |
23 | export default meta;
24 |
25 | const Template: Story = args => (
26 |
27 | );
28 |
29 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
30 | // https://storybook.js.org/docs/react/workflows/unit-testing
31 | export const Default = Template.bind({});
32 |
33 | Default.args = {
34 | number: '532385901',
35 | position: '-66px -69px',
36 | tooltip: 'I likey',
37 | active: true,
38 | };
39 |
--------------------------------------------------------------------------------
/stories/active.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, Story } from '@storybook/react';
2 | import React from 'react';
3 | import withActive from '../src/helpers/withActive';
4 |
5 | interface ThingProps {
6 | message: string;
7 | }
8 |
9 | const Thing = withActive(({ message }: ThingProps) => {
10 | return {message}
;
11 | });
12 |
13 | const meta: Meta = {
14 | title: 'withActive',
15 | component: Thing,
16 | argTypes: {
17 | children: {
18 | control: {
19 | type: 'text',
20 | },
21 | },
22 | },
23 | parameters: {
24 | controls: { expanded: true },
25 | },
26 | };
27 |
28 | export default meta;
29 |
30 | const Template: Story = args => {
31 | return ;
32 | };
33 |
34 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
35 | // https://storybook.js.org/docs/react/workflows/unit-testing
36 | export const Default = Template.bind({});
37 |
38 | Default.args = {
39 | message: 'Test',
40 | };
41 |
--------------------------------------------------------------------------------
/stories/helper.css:
--------------------------------------------------------------------------------
1 | /* center components */
2 | .center {
3 | width: 100%;
4 | font-family: sans-serif;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | align-items: center;
9 | margin: 2em;
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
3 | "include": ["src", "types"],
4 | "compilerOptions": {
5 | "module": "esnext",
6 | "lib": ["dom", "esnext"],
7 | "importHelpers": true,
8 | // output .d.ts declaration files for consumers
9 | "declaration": true,
10 | // output .js.map sourcemap files for consumers
11 | "sourceMap": true,
12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index
13 | "rootDir": "./src",
14 | // stricter type-checking for stronger correctness. Recommended by TS
15 | "strict": true,
16 | // linter checks for common issues
17 | "noImplicitReturns": true,
18 | "noFallthroughCasesInSwitch": true,
19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | // use Node's module resolution algorithm, instead of the legacy TS one
23 | "moduleResolution": "node",
24 | // transpile JSX to React.createElement
25 | "jsx": "react",
26 | // interop between ESM and CJS modules. Recommended by TS
27 | "esModuleInterop": true,
28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
29 | "skipLibCheck": true,
30 | // error out if import and file system have a casing mismatch. Recommended by TS
31 | "forceConsistentCasingInFileNames": true,
32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
33 | "noEmit": true,
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tsdx.config.js:
--------------------------------------------------------------------------------
1 | var postcss = require('rollup-plugin-postcss');
2 | var autoprefixer = require('autoprefixer');
3 | var cssnano = require('cssnano');
4 | var atImport = require('postcss-import');
5 |
6 | module.exports = {
7 | rollup(config, options) {
8 | config.plugins.push(
9 | postcss({
10 | plugins: [
11 | atImport(),
12 | autoprefixer(),
13 | cssnano({
14 | preset: 'default',
15 | }),
16 | ],
17 | inject: false,
18 | // only write out CSS for the first bundle (avoids pointless extra files):
19 | extract: !!options.writeMeta,
20 | })
21 | );
22 | return config;
23 | },
24 | };
25 |
--------------------------------------------------------------------------------