├── .babelrc ├── .gitignore ├── .npmignore ├── README.md ├── example └── src │ ├── app.js │ └── index.html ├── package.json ├── publish.config.js ├── src ├── index.css ├── index.js └── send-icon.svg ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # npm 15 | /dist 16 | 17 | # ide 18 | /.idea 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | /example/dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # ide 15 | /.idea 16 | 17 | /src 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | /example -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-custom-chat 2 | 3 | react-custom-chat is a free and fully customizable chat window that can be easily included in any project. 4 | 5 | ![Demo gif of react-custom-chat being used](https://user-images.githubusercontent.com/43746609/116437983-ea2d9e00-a845-11eb-8297-272ee0eb00d2.gif) 6 | 7 | ### [Link to Repository](https://github.com/ebenezerdon/react-custom-chat.git) 8 | ## [Demo](https://github.com/ebenezerdon/react-custom-chat) 9 | 10 | ## Table of Contents 11 | - [Installation](#installation) 12 | - [Example](#example) 13 | - [Components](#components) 14 | 15 | ## Installation 16 | 17 | ``` 18 | $ npm install react-custom-chat 19 | ``` 20 | 21 | ## Example 22 | 23 | ``` javascript 24 | import ChatBox from 'react-custom-chat' 25 | import { useState } from 'react' 26 | 27 | const App = () => { 28 | const [messageList, setMessageList] = useState([]) 29 | 30 | const handleSendMessage = newMessage => { 31 | setMessageList([ 32 | ...messageList, 33 | {text: newMessage, person: 'primary'} 34 | ]) 35 | } 36 | 37 | return ( 38 |
39 | 48 |
49 | ) 50 | } 51 | ``` 52 | 53 | ## Components 54 | 55 | # ChatBox 56 | 57 | ### ChatBox props: 58 | 59 | | prop | type | required | description | 60 | |------------------|--------|----------|-------------| 61 | | settings | [object](#settings-properties) | no | You can use this prop to define custom settings for your chat window. | 62 | | messageList | array | yes | an array of objects for your messages. See message object [here](#message-object) | 63 | | isOpen | boolean | yes | Forces the open/close state of the chat window. If this is not set, it will be closed by default, and open when the chat icon is clicked. | 64 | | onSendMessage | function | yes | The function's first parameter will be the message from the input field. see [example](#example) for usage. 65 | 66 | 67 | ### Settings properties: 68 | 69 | | property | type | description | 70 | |------------------|--------|-------------| 71 | | position | string | This indicates the position of your chat window. Value can be `left` or `right`. Default is `right`. | 72 | | navColor | string | You can use this to customize your NavColor | 73 | | primaryColor | string | Use this to define the primary user color. You can choose from 'green', 'red', 'blue', etc... | 74 | | secondaryColor | string | Use this to define the secondary user color. You can choose from 'green', 'red', 'blue', etc... | 75 | | navText | string | This is the text that shows up on the chat window header. The Default is 'Jane Doe'| 76 | 77 | 78 | ### Message Object 79 | 80 | Each message is styled differently depending on its type. Currently, only text is supported. 81 | Each message object has a `person` property which can have the value 'primary' or 'secondary', 82 | and a `text` property which contains the message text. 83 | 84 | ``` javascript 85 | { 86 | text: 'hello world!', 87 | person: 'primary', 88 | } 89 | ``` 90 | 91 | ## Using react-custom-chat? Let's talk 92 | 93 | If you're using react-custom-chat, I'd love to see what you're building! Send me a mail 94 | -------------------------------------------------------------------------------- /example/src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import ChatBox from '../../src' 4 | 5 | const App = () => { 6 | const [messageList, setMessageList] = React.useState([]) 7 | 8 | const handleSendMessage = newMessage => { 9 | setMessageList([ 10 | ...messageList, 11 | {text: newMessage, person: 'primary'} 12 | ]) 13 | } 14 | 15 | return ( 16 |
17 | 26 |
27 | ) 28 | } 29 | 30 | render(, document.getElementById('root')) 31 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | My First React Component 4 | 5 | 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-custom-chat", 3 | "version": "0.6.1", 4 | "description": "a free and fully customizable chat window that can be easily included in any project", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server --open development", 9 | "build": "webpack --mode production", 10 | "deploy": "webpack --config publish.config.js --mode production && npm publish" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "chat", 15 | "chat widget", 16 | "chat window", 17 | "intercom", 18 | "react custom chat", 19 | "hooks", 20 | "functional react" 21 | ], 22 | "author": "Ebenezer Don", 23 | "license": "ISC", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/ebenezerdon/react-custom-chat.git" 27 | }, 28 | "peerDependencies": { 29 | "react": "^17.0.2", 30 | "react-dom": "^17.0.2" 31 | }, 32 | "devDependencies": { 33 | "@babel/cli": "^7.5.5", 34 | "@babel/core": "^7.5.5", 35 | "@babel/preset-env": "^7.5.5", 36 | "@babel/preset-react": "^7.0.0", 37 | "babel-loader": "^8.0.6", 38 | "css-loader": "^5.2.4", 39 | "html-webpack-plugin": "^3.2.0", 40 | "react": "^17.0.2", 41 | "react-dom": "^17.0.2", 42 | "style-loader": "^2.0.0", 43 | "svg-url-loader": "^7.1.1", 44 | "webpack": "^4.39.3", 45 | "webpack-cli": "^3.3.7", 46 | "webpack-dev-server": "^3.8.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /publish.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: path.join(__dirname, "./src/index.js"), 5 | output: { 6 | path: path.join(__dirname, "./dist"), 7 | filename: "index.js", 8 | libraryTarget: 'commonjs2' 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx)$/, 14 | use: "babel-loader", 15 | exclude: /node_modules/ 16 | }, 17 | { 18 | test: /\.css$/i, 19 | use: ["style-loader", "css-loader"], 20 | }, 21 | { 22 | test: /\.svg$/, 23 | use: "svg-url-loader" 24 | } 25 | ] 26 | }, 27 | resolve: { 28 | extensions: [".js", ".jsx"] 29 | }, 30 | externals: { 31 | 'react': 'commonjs react' 32 | } 33 | }; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /*! tailwindcss v2.1.2 | MIT License | https://tailwindcss.com */ 2 | 3 | /*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */:root{-moz-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.bg-black{--tw-bg-opacity:1;background-color:rgba(0,0,0,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgba(107,114,128,var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgba(75,85,99,var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgba(239,68,68,var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220,38,38,var(--tw-bg-opacity))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgba(245,158,11,var(--tw-bg-opacity))}.bg-yellow-600{--tw-bg-opacity:1;background-color:rgba(217,119,6,var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgba(16,185,129,var(--tw-bg-opacity))}.bg-green-600{--tw-bg-opacity:1;background-color:rgba(5,150,105,var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgba(59,130,246,var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgba(37,99,235,var(--tw-bg-opacity))}.bg-purple-500{--tw-bg-opacity:1;background-color:rgba(139,92,246,var(--tw-bg-opacity))}.rounded-lg{border-radius:.5rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.table{display:table}.grid{display:grid}.float-right{float:right}.clear-both{clear:both}.h-12{height:3rem}.h-16{height:4rem}.h-4\/6{height:66.666667%}.h-5\/6{height:83.333333%}.text-2xl{font-size:1.5rem;line-height:2rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.max-w-xs{max-width:20rem}.overflow-auto{overflow:auto}.p-2{padding:.5rem}.p-4{padding:1rem}.pt-4{padding-top:1rem}.absolute{position:absolute}.right-5{right:1.25rem}.left-5{left:1.25rem}.bottom-8{bottom:2rem}.bottom-32{bottom:8rem}*{--tw-shadow:0 0 transparent}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,0.25)}.shadow-2xl,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,0.06)}*{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,0.5);--tw-ring-offset-shadow:0 0 transparent;--tw-ring-shadow:0 0 transparent}.text-center{text-align:center}.text-white{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgba(31,41,55,var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgba(153,27,27,var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgba(146,64,14,var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgba(6,95,70,var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgba(30,64,175,var(--tw-text-opacity))}.w-16{width:4rem}.w-96{width:24rem}.w-full{width:100%}.w-max{width:-webkit-max-content;width:-moz-max-content;width:max-content}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.col-span-5{grid-column:span 5/span 5}.transform{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-150{--tw-scale-x:1.5;--tw-scale-y:1.5}@keyframes spin{to{transform:rotate(1turn)}}@keyframes ping{75%,to{transform:scale(2);opacity:0}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.fade-in{opacity:1;animation-name:fadeInOpacity;animation-iteration-count:1;animation-timing-function:ease-in;animation-duration:.2s}@keyframes fadeInOpacity{0%{opacity:0}to{opacity:1}} 4 | /*# sourceMappingURL=main.3bce4cc5.chunk.css.map */ -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useEffect, useState } from 'react' 3 | import sendIcon from './send-icon.svg' 4 | import './index.css' 5 | 6 | const ChatBox = ( { settings, messageList, onSendMessage } ) => { 7 | const { position, navColor, secondaryColor, primaryColor, navText, isOpen } = settings || {} 8 | const uniqueId = 'id' + Math.random().toString(36).substr(2, 5) 9 | const [chatBoxIsActive, setChatBoxIsActive] = useState(isOpen || false) 10 | 11 | const styles = { 12 | wrapper: `fade-in absolute rounded-lg h-4/6 w-96 shadow-2xl bottom-32 ${position === 'left' ? 'left' : 'right'}-5`, 13 | nav: `h-16 w-full bg-${navColor || 'blue'}-500 text-white pt-4 rounded-t-lg`, 14 | navText: `text-center text-2xl`, 15 | messages: `p-4 h-5/6 overflow-auto text-white`, 16 | personLeft: `fade-in bg-${secondaryColor || 'gray'}-600 p-2 w-max max-w-xs rounded-lg clear-both mb-2`, 17 | personRight: `fade-in bg-${primaryColor || 'red'}-600 p-2 w-max max-w-xs rounded-lg clear-both float-right mb-2`, 18 | formWrapper: `h-12 bg-gray-200`, 19 | form: `grid grid-cols-6`, 20 | inputField: `bg-gray-200 h-12 shadow-inner w-full p-4 col-span-5`, 21 | toggleButton: `absolute bottom-8 ${position === 'left' ? 'left' : 'right'}-5`, 22 | sendIcon: `transform scale-150 text-align mx-auto`, 23 | chatIcon: `h-16 w-16 text-${navColor || 'blue'}-800` 24 | } 25 | 26 | const handleFormSubmit = event => { 27 | event.preventDefault() 28 | const chatInput = document.getElementById(uniqueId) 29 | onSendMessage?.(chatInput.value) 30 | event.target.reset() 31 | } 32 | 33 | useEffect(() => { 34 | // scroll to last message 35 | const messageSection = document.getElementById('msg' + uniqueId) 36 | messageSection?.scrollTo(0, messageSection.scrollHeight) 37 | }, [uniqueId]) 38 | 39 | return ( 40 | <> 41 | {chatBoxIsActive &&
42 |
43 |

{navText || 'Jane Doe'}

44 |
45 |
46 | {messageList?.map((message, index) => ( 47 |

50 | {message.text} 51 |

52 | ))} 53 |
54 |
55 |
handleFormSubmit(event)}> 56 | 57 | 60 |
61 |
62 |
} 63 | 78 | 79 | ) 80 | } 81 | 82 | export default ChatBox; 83 | -------------------------------------------------------------------------------- /src/send-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const htmlWebpackPlugin = new HtmlWebpackPlugin({ 4 | template: path.join(__dirname, "./example/src/index.html"), 5 | filename: "./index.html" 6 | }); 7 | 8 | module.exports = { 9 | entry: path.join(__dirname, "./example/src/app.js"), 10 | output: { 11 | path: path.join(__dirname, "example/dist"), 12 | filename: "bundle.js" 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(js|jsx)$/, 18 | use: "babel-loader", 19 | exclude: /node_modules/ 20 | }, 21 | { 22 | test: /\.css$/i, 23 | use: ["style-loader", "css-loader"], 24 | }, 25 | { 26 | test: /\.svg$/, 27 | use: "svg-url-loader" 28 | } 29 | ] 30 | }, 31 | plugins: [htmlWebpackPlugin], 32 | resolve: { 33 | extensions: [".js", ".jsx"] 34 | }, 35 | devServer: { 36 | port: 3001 37 | } 38 | }; --------------------------------------------------------------------------------