├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── example ├── README.md ├── package.json ├── public │ ├── index.html │ └── manifest.json ├── src │ ├── App.js │ ├── assets │ │ └── chat_avatar.png │ ├── index.css │ └── index.js └── yarn.lock ├── lab_signum.png ├── lab_signum_configurator.png ├── package.json ├── rollup.config.js ├── src ├── common.d.ts ├── components │ ├── chat-body │ │ ├── index.tsx │ │ └── style.scss │ ├── chat-context │ │ ├── ChatContext.ts │ │ └── index.tsx │ └── chat-icon │ │ ├── assets │ │ ├── chat_icon.svg │ │ └── chat_icon_close.svg │ │ ├── index.tsx │ │ └── style.scss ├── constants │ └── index.ts ├── index.tsx └── types.ts ├── tsconfig.json ├── tsconfig.test.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | env: 5 | - SKIP_PREFLIGHT_CHECK=true 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Signum 2 | ![Banner](https://github.com/nightborn-be/signum/blob/master/lab_signum.png) 3 | 4 | [![NPM](https://img.shields.io/npm/v/@nightborn/signum.svg)](https://www.npmjs.com/package/@nightborn/signum) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 5 | 6 | ## Demo 7 | 8 | - You can try the communication tool here: https://nightborn-be.github.io/signum 9 | - Or on our website for a production-version: https://nightborn.be 10 | 11 | ## Install 12 | 13 | ```bash 14 | npm install --save @nightborn/signum 15 | yarn add @nightborn/signum 16 | ``` 17 | 18 | ## Configuration 19 | 20 | ### Theme 21 | ![Configurator](https://github.com/nightborn-be/signum/blob/master/lab_signum_configurator.png) 22 | For the configuration of the UI we created a little interface to generate a config.js file to customize the theme. 23 | https://nightborn.be/lab/signum 24 | 25 | ### Options 26 | For the options it's a pretty straightforward structure. 27 | 28 | 1. First create the container object: 29 | 30 | ```jsx 31 | option: { 32 | title: 'Hello there !', 33 | subTitle: 'How can I help you?', 34 | message: 'How can I help you?', 35 | name: 'Hello there' 36 | } 37 | ``` 38 | 39 | 2. Add the array of options (If you want options) 40 | ```jsx 41 | option: { 42 | title: 'Hello there !', 43 | subTitle: 'How can I help you?', 44 | message: 'How can I help you?', 45 | name: 'Hello there', 46 | options: [ 47 | { 48 | title: 'Hello there !', 49 | message: 'You chose option A?', 50 | name: 'Option A', 51 | options: [ 52 | { 53 | title: 'Hello there !', 54 | message: 'You chose option AA?', 55 | name: 'Option AA', 56 | }, 57 | { 58 | title: 'Hello there !', 59 | message: 'You chose option AB?', 60 | name: 'Option AB', 61 | } 62 | ] 63 | }, 64 | { 65 | title: 'Hello there !', 66 | message: 'You chose option B?', 67 | name: 'Option B', 68 | } 69 | ] 70 | } 71 | ``` 72 | 73 | 74 | ### Important information 75 | 76 | 1. In order for the back-button to appear, you should leave blanc the subTitle attribute of the object. 77 | 2. If you're at a leaf of your option-tree, please leave empty the options attribute and the messaging body of Signum will appear. 78 | 79 | 80 | ## Usage 81 | 82 | ```jsx 83 | import React from 'react' 84 | import Chat from '@nightborn/signum' 85 | import '@nightborn/signum/dist/index.css'; 86 | 87 | const defaultProps = { 88 | option: { 89 | title: 'Hello there !', 90 | subTitle: 'How can I help you?', 91 | message: 'How can I help you?', 92 | name: 'Hello there', 93 | options: [ 94 | { 95 | title: 'Hello there !', 96 | message: 'You chose option A?', 97 | name: 'Option A', 98 | options: [ 99 | { 100 | title: 'Hello there !', 101 | message: 'You chose option AA?', 102 | name: 'Option AA', 103 | }, 104 | { 105 | title: 'Hello there !', 106 | message: 'You chose option AB?', 107 | name: 'Option AB', 108 | } 109 | ] 110 | }, 111 | { 112 | title: 'Hello there !', 113 | message: 'You chose option B?', 114 | name: 'Option B', 115 | } 116 | ] 117 | }, 118 | config: { 119 | openByDefault: false, 120 | avatarIcon: require('./assets/chat_avatar.png'), 121 | mainColor: 'linear-gradient(90deg, #406321 0%, #283E15 100%)', 122 | secondaryColor: 'linear-gradient(90deg, #406321 0%, #283E15 100%)', 123 | sendButtonColor: '#0074CE', 124 | finalButtonColor: "linear-gradient(90deg, #406321 0%, #283E15 100%)", 125 | emailPlaceholder: 'Please fill in your e-mail', 126 | messagePlaceholder: 'Please provide us some information', 127 | finalTitle: 'Thank you.', 128 | finalSubTitle: "We'll be in touch!", 129 | finalButtonText: "Continue", 130 | handleFinalButtonClicked: () => { }, 131 | handleSendClicked: (information) => console.log(information), 132 | } 133 | } 134 | 135 | const App = () => { 136 | return ( 137 | 138 | ) 139 | } 140 | 141 | export default App; 142 | ``` 143 | 144 | ## Result 145 | 146 | In the handleSendClicked function you'll receive following structure 147 | ```jsx 148 | interface IChatBodyInformation { 149 | email: string, 150 | message: string, 151 | steps: IOption[], 152 | option: IOption 153 | } 154 | ``` 155 | You can then use the received information to send it to EMAILJS, your backend, ... any solution in which you want to track the information given by your website visitors. 156 | 157 | ## License 158 | 159 | MIT © [m-Nightly](https://github.com/m-Nightly) 160 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | Example project of Signum usage. 2 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "signum-example", 3 | "homepage": "https://m-Nightly.github.io/signum", 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "private": true, 7 | "dependencies": { 8 | "signum": "link:..", 9 | "react": "link:../node_modules/react", 10 | "react-dom": "^16.9.0", 11 | "react-scripts": "^3.0.1" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject" 18 | }, 19 | "browserslist": [ 20 | ">0.2%", 21 | "not dead", 22 | "not ie <= 11", 23 | "not op_mini all" 24 | ] 25 | } -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | signum 11 | 12 | 13 | 14 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "signum", 3 | "name": "signum", 4 | "start_url": "./index.html", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Chat from 'signum' 3 | import 'signum/dist/index.css'; 4 | 5 | const defaultProps = { 6 | option: { 7 | title: 'Hello there !', 8 | subTitle: 'How can I help you?', 9 | message: 'How can I help you?', 10 | name: 'Hello there', 11 | options: [ 12 | { 13 | title: 'Hello there !', 14 | message: 'You chose option A?', 15 | name: 'Option A', 16 | options: [ 17 | { 18 | title: 'Hello there !', 19 | message: 'You chose option AA?', 20 | name: 'Option AA', 21 | }, 22 | { 23 | title: 'Hello there !', 24 | message: 'You chose option AB?', 25 | name: 'Option AB', 26 | } 27 | ] 28 | }, 29 | { 30 | title: 'Hello there !', 31 | message: 'You chose option B?', 32 | name: 'Option B', 33 | } 34 | ] 35 | }, 36 | config: { 37 | avatarIcon: require('./assets/chat_avatar.png'), 38 | mainColor: 'linear-gradient(90deg, #406321 0%, #283E15 100%)', 39 | secondaryColor: 'linear-gradient(90deg, #406321 0%, #283E15 100%)', 40 | sendButtonColor: '#0074CE', 41 | finalButtonColor: "linear-gradient(90deg, #406321 0%, #283E15 100%)", 42 | emailPlaceholder: 'Please fill in your e-mail', 43 | messagePlaceholder: 'Please provide us some information', 44 | finalTitle: 'Thank you.', 45 | finalSubTitle: "We'll be in touch!", 46 | finalButtonText: "Continue", 47 | handleFinalButtonClicked: () => { }, 48 | handleSendClicked: (information) => console.log(information), 49 | } 50 | } 51 | 52 | const App = () => { 53 | return ( 54 | 55 | ) 56 | } 57 | 58 | export default App; 59 | -------------------------------------------------------------------------------- /example/src/assets/chat_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nightborn-be/signum/eb55362b7157f9a8ebce43fcc3457de38e975a8c/example/src/assets/chat_avatar.png -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | .app-container { 8 | height: 100%; 9 | width: 100%; 10 | background: red; 11 | } -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import './index.css' 5 | import App from './App' 6 | 7 | ReactDOM.render(, document.getElementById('root')) 8 | -------------------------------------------------------------------------------- /lab_signum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nightborn-be/signum/eb55362b7157f9a8ebce43fcc3457de38e975a8c/lab_signum.png -------------------------------------------------------------------------------- /lab_signum_configurator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nightborn-be/signum/eb55362b7157f9a8ebce43fcc3457de38e975a8c/lab_signum_configurator.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nightborn/signum", 3 | "version": "1.0.6", 4 | "description": "A lead-generating communication component", 5 | "author": "Maxime Denuit", 6 | "license": "MIT", 7 | "private": false, 8 | "repository": "nightborn-be/signum", 9 | "main": "dist/index.js", 10 | "types": "./dist/index.d.ts", 11 | "module": "dist/index.es.js", 12 | "jsnext:main": "dist/index.es.js", 13 | "files": [ 14 | "dist" 15 | ], 16 | "engines": { 17 | "node": ">=8", 18 | "npm": ">=5" 19 | }, 20 | "scripts": { 21 | "build": "rollup -c", 22 | "start": "rollup -c -w", 23 | "prepare": "yarn run build", 24 | "predeploy": "cd example && yarn install && yarn run build", 25 | "deploy": "gh-pages -d example/build" 26 | }, 27 | "dependencies": { 28 | "@babel/preset-react": "^7.9.4", 29 | "@rollup/plugin-image": "^2.0.4", 30 | "@rollup/plugin-typescript": "^4.0.0", 31 | "@wessberg/rollup-plugin-ts": "^1.2.24", 32 | "framer-motion": "^1.10.3", 33 | "rollup": "^2.4.0", 34 | "rollup-plugin-babel": "^4.4.0", 35 | "rollup-plugin-scss": "^2.1.0" 36 | }, 37 | "peerDependencies": { 38 | "react": "^16.9.0" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.2.2", 42 | "@babel/runtime": "^7.3.1", 43 | "@testing-library/react-hooks": "^3.2.1", 44 | "@types/jest": "^23.3.13", 45 | "@types/react": "^16.7.22", 46 | "cross-env": "^5.2.0", 47 | "gh-pages": "^2.0.1", 48 | "typescript": "^3.2.4" 49 | } 50 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import ts from "@wessberg/rollup-plugin-ts"; 2 | import pkg from './package.json'; 3 | import babel from 'rollup-plugin-babel'; 4 | import scss from 'rollup-plugin-scss' 5 | import image from '@rollup/plugin-image'; 6 | 7 | const external = Object.keys(pkg.dependencies).concat(['path', 'fs', 'typescript']); 8 | 9 | export default { 10 | input: 'src/index.tsx', 11 | plugins: [ts(), babel({ 12 | exclude: 'node_modules/**', 13 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 14 | presets: ['@babel/env', '@babel/preset-react'] 15 | 16 | }), scss(), image()], 17 | external, 18 | output: [ 19 | { format: 'cjs', file: pkg.main }, 20 | { format: 'esm', file: pkg.module } 21 | ] 22 | }; -------------------------------------------------------------------------------- /src/common.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | import React = require("react"); 3 | export const ReactComponent: React.FunctionComponent>; 6 | const src: string; 7 | export default src; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/chat-body/index.tsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | import React, { useState, useEffect } from 'react'; 3 | import { motion, useAnimation } from 'framer-motion'; 4 | import { useChat } from '../chat-context/ChatContext'; 5 | import { IOption } from '../../types'; 6 | 7 | export default function ChatBody() { 8 | 9 | // Attributes 10 | const { isOpen, closeChat, config, defaultOption } = useChat(); 11 | 12 | const apparitionAnimation = useAnimation(); 13 | const apparitionHeaderAnimation = useAnimation(); 14 | const apparitionBodyAnimation = useAnimation(); 15 | const apparitionFooterAnimation = useAnimation(); 16 | const [option, setOption] = useState(defaultOption); 17 | const [steps, setSteps] = useState([]); 18 | const [email, setEmail] = useState(''); 19 | const [message, setMessage] = useState(''); 20 | const [isDone, setIsDone] = useState(false); 21 | 22 | // Effects 23 | useEffect(() => { 24 | if (!isOpen) { 25 | outSequence(); 26 | } else { 27 | inSequence(); 28 | } 29 | }, [isOpen]) 30 | 31 | // Methods 32 | async function inSequence() { 33 | await apparitionAnimation.start({ scaleY: 0, opacity: 0, x: 0, transition: { duration: 0 } }); 34 | apparitionAnimation.start({ opacity: 1, scaleY: 1, transition: { duration: 0.2 } }); 35 | apparitionHeaderAnimation.start({ opacity: 1, y: 0, transition: { delay: 0.2 } }); 36 | apparitionBodyAnimation.start({ y: 0, opacity: 1, transition: { delay: 0.2 } }); 37 | apparitionFooterAnimation.start({ y: 0, opacity: 1, scale: 1, transition: { delay: 0.2, ease: "linear" } }); 38 | } 39 | 40 | async function outSequence() { 41 | await apparitionAnimation.start({ scaleY: 1, opacity: 0, x: 450, transition: { duration: 0.5 } }); 42 | apparitionHeaderAnimation.start({ opacity: 0, y: 10, transition: { duration: 0.3 } }); 43 | apparitionBodyAnimation.start({ y: -10, opacity: 0, transition: { duration: 0.3 } }); 44 | apparitionFooterAnimation.start({ y: 10, opacity: 0, transition: { duration: 0.3, ease: "linear" } }); 45 | } 46 | 47 | function handleOptionClicked(option: IOption) { 48 | setSteps((steps) => { steps.push(option); return steps; }); 49 | setOption(option); 50 | } 51 | 52 | function handleBackClicked() { 53 | // Resets the current values 54 | setMessage(''); 55 | setEmail(''); 56 | 57 | // Sets the current option to the previous one 58 | if (steps.length >= 2) { 59 | setOption(steps[steps.length - 2]); 60 | } 61 | else { 62 | setOption(defaultOption); 63 | } 64 | 65 | // Removes the previous step 66 | setSteps((steps) => { 67 | steps.pop(); 68 | return steps; 69 | }); 70 | } 71 | 72 | async function handleSendClicked() { 73 | if (isFormFilledIn()) { 74 | await config.handleSendClicked({ email, message, steps, option }); 75 | setIsDone(true); 76 | } 77 | } 78 | 79 | async function handleFinishClicked() { 80 | await outSequence(); 81 | reset(); 82 | if (config.handleFinalButtonClicked) { 83 | config.handleFinalButtonClicked(); 84 | } 85 | } 86 | 87 | /** 88 | * Returns if the message and e-mail are filled in 89 | */ 90 | function isFormFilledIn() { 91 | if (message && email) { 92 | return true 93 | } 94 | return false; 95 | } 96 | 97 | /** 98 | * Resets the entire state of the chat 99 | */ 100 | function reset() { 101 | closeChat(); 102 | setOption(defaultOption); 103 | setSteps([]); 104 | setIsDone(false); 105 | setEmail(''); 106 | setMessage(''); 107 | } 108 | 109 | if (!isOpen) { 110 | return ( 111 | <> 112 | 113 | ) 114 | } 115 | 116 | // Render 117 | if (isDone) { 118 | return ( 119 | 120 |
121 | {config.finalTitle} 122 |
123 |
124 | {config.finalSubTitle} 125 |
126 | 127 | {config.finalButtonText} 128 | 129 |
130 | ); 131 | } 132 | 133 | return ( 134 | 135 |
136 | 137 | { 138 | option.subTitle ? 139 | ( 140 |
141 |
142 |
143 | {option.title} 144 |
145 |
146 | 147 | 148 | 149 |
150 |
151 |
152 | {option.subTitle} 153 |
154 |
155 | ) 156 | : 157 | ( 158 |
159 |
160 | 161 | 162 | 163 | 164 | 165 |
166 | {option.title} 167 |
168 |
169 |
170 | 171 | 172 | 173 |
174 |
175 | ) 176 | } 177 |
178 |
179 | 180 |
181 | 182 |
183 | {option.message} 184 |
185 |
186 | 187 |
188 | {option.options && option.options.map((option) => ( 189 | handleOptionClicked(option)} className="chat-body-content-option" style={{ background: config.mainColor }}> 190 | {option.name} 191 | 192 | ))} 193 |
194 |
195 | 196 | { 197 | option.options === undefined ? 198 | ( 199 | 200 |
201 | setEmail(event.target.value)} placeholder={config.emailPlaceholder} /> 202 |