Skip to content

Instantly share code, notes, and snippets.

@marcantoine
Last active June 15, 2018 18:15
Show Gist options
  • Save marcantoine/1c9747b1f6807eb8b409b6406c2f962b to your computer and use it in GitHub Desktop.
Save marcantoine/1c9747b1f6807eb8b409b6406c2f962b to your computer and use it in GitHub Desktop.
//the sass file with all the style for the feedback box, nice
:root{
--feedback-notice: rgba(0, 0, 0, 0.5);
--feedback-action: rgba(107, 160, 222, 1);
--feedback-action-active: rgba(123, 174, 231, 1);
--feedback-disabled: rgba(0,0,0,0.3);
--feedback-disabled-background: rgba(0,0,0,0.2);
--feedback-error: rgba(245, 89, 89, 1);
--feedback-border: rgb(242, 242, 242, 1);
--feedback-button-text : rgba(255,255,255,1);
--feedback-hover: rgba(250,250,250,1);
--feedback-background: rgba(255,255,255,1);
}
.feedback{
position: fixed;
bottom: -1px;
z-index: 999999999;
width: 240px;
margin: 0 1em;
display: flex;
flex-flow: column;
box-shadow: 0px -1px 6px 0px rgba(50, 69, 93, 0.15), 0px -1px 3px 0px rgba(0, 0, 0, 0.08);
animation: fadeOut 0.375s;
&.active{
height: 360px;
width: 360px;
&.error{
height: 380px;
}
.feedback__trigger:hover{
transform: none;
background-color: var(--feedback-hover);
transform: translateY(1px);
}
.feedback__container{
height: 100%;
opacity: 1;
padding: 0 1em;
}
.feedback__arrow{
transform: rotate(180deg);
}
animation: fadeIn 0.375s;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
transform: translate3d(0, 10%, 0);
}
100% {
opacity: 1;
transform: none;
}
}
@keyframes fadeOut {
0% {
opacity: 0;
transform: translate3d(0, -30%, 0);
}
100% {
opacity: 1;
transform: none;
}
}
.feedback__trigger{
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
background: var(--feedback-background);
padding: 1em;
border-radius: 4px 4px 0 0;
&:hover{
transform: translateY(-1px);
}
}
.feedback__arrow{
height: 12px;
}
.feedback__container{
background: var(--feedback-background);
height: 0;
opacity: 0;
}
.feedback__intro, .feedback__success,{
font-size: 12px;
color: var(--feedback-notice);
margin: 0 0 0.5 0em;
width: 100%;
}
.feedback__error{
font-size: 12px;
color: var(--feedback-error);
margin: 0 0 0.5 0em;
width: 100%;
}
.feedback__form{
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
}
.feedback__input{
display: block;
box-sizing: border-box;
width: 100%;
font-size: 0.8em;
padding: 0.5em;
border-radius: 0.2em;
border: 1px rgba(0, 0, 0, 0.1) solid;
background-color: var(--feedback-background);
margin-bottom: .5em;
&:focus{
outline: none;
box-shadow: 0px 4px 6px 0px rgba(50, 69, 93, 0.30), 0px 1px 3px 0px rgba(0, 0, 0, 0.16);
}
&.error{
border-color: var(--feedback-error);
outline: none;
}
}
.feedback__content{
resize: none;
height: 120px;
}
.feedback__submit{
padding: 6px 0.2em;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 13px;
line-height: 16px;
font-weight: 100;
text-transform: uppercase;
cursor: pointer;
padding-left: 0.75em;
padding-right: 0.75em;
border: none;
text-decoration: none ;
border-radius: 2em;
color: var(--feedback-button-text);
background-color: var(--feedback-action);
transition: background-color 300ms ease;
&--none{
display: none;
}
&:focus{
outline: none;
border-radius: 2em;
box-shadow: 0px 4px 6px 0px rgba(50, 69, 93, 0.30), 0px 1px 3px 0px rgba(0, 0, 0, 0.16);
}
&:hover{
background-color: var(--feedback-action-active);
border-color: var(--feedback-border);
}
&:disabled{
border-color: var(--feedback-border);
color: var(--feedback-disabled);
background-color: var(--feedback-disabled-background);
}
}
.feedback__loader {
display: inline-block;
position: relative;
width: 64px;
height: 1.5rem;
&--none{
display: none;
}
}
.feedback__loader div {
position: absolute;
top: 6px;
width: 11px;
height: 11px;
border-radius: 50%;
background: var(--feedback-action);
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.feedback__loader div:nth-child(1) {
left: 6px;
animation: feedback__loader1 0.6s infinite;
}
.feedback__loader div:nth-child(2) {
left: 6px;
animation: feedback__loader2 0.6s infinite;
}
.feedback__loader div:nth-child(3) {
left: 26px;
animation: feedback__loader2 0.6s infinite;
}
.feedback__loader div:nth-child(4) {
left: 45px;
animation: feedback__loader3 0.6s infinite;
}
@keyframes feedback__loader1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes feedback__loader2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(19px, 0);
}
}
@keyframes feedback__loader3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
//so i made a specific file for this bot cause i will want to add some other stuff in there
//its a bit useless for now and could totally be in the feedbackController file
// also please install telegraf : https://www.npmjs.com/package/telegraf
const Telegraf = require('telegraf')
// the bot token botfather give you when creating the bot.
// To create the bot, just talk to @botfather and type follow the instruction
//After setting the name and username you can access the token
const bot = new Telegraf(process.env.BOT_TOKEN)
bot.start((ctx) => ctx.reply('Welcome'))
bot.startPolling()
module.exports.bot = bot
// A vanilla JS module to create the feedback box on the website.
// i should have put all the text in the default props, i forgot
const defaultProps = {
buttonText:'Have feedback?',
}
const feedback = function(){
if(!document.getElementById('feedback')) init()
let state = {
active : false,
sent: false,
}
// I create the box for the first time using the trigger and container "components"
function init(){
let feedbackBox = document.createElement('div')
feedbackBox.id = 'feedback'
feedbackBox.className = 'feedback';
let trigger = triggerComponent()
let container = containerComponent()
feedbackBox.appendChild(trigger)
feedbackBox.appendChild(container)
document.body.appendChild(feedbackBox)
}
function triggerComponent(){
let t = document.createElement('div')
t.className = 'feedback__trigger'
let text = document.createElement('span')
text.innerHTML += defaultProps.buttonText
t.appendChild(text)
t.innerHTML += '<svg class="feedback__arrow" width="36px" height="21px" viewBox="0 0 36 21" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M27.6362501,8.33058556 L12.6901773,-6.60972143 C11.5026811,-7.79675952 9.57811834,-7.79675952 8.39062214,-6.60972143 C7.20312595,-5.42268334 7.20312595,-3.49886299 8.39062214,-2.35275725 L21.2483395,10.5 L8.39062214,23.3527572 C7.20312595,24.5397953 7.20312595,26.4636157 8.39062214,27.6097214 C9.57811834,28.7967595 11.5026811,28.7967595 12.6492292,27.6097214 L27.6362501,12.6694144 C28.2504722,12.0554292 28.5371093,11.2777146 28.4961611,10.5 C28.5371093,9.72228539 28.2095241,8.94457078 27.6362501,8.33058556 Z" id="Shape" fill="#89B5FF" fill-rule="nonzero" transform="translate(18.000000, 10.500000) rotate(-90.000000) translate(-18.000000, -10.500000) "></path></svg>'
t.addEventListener('click', function(){
toggle(this)
})
return(t)
}
// When creating the container , i create the form inside as well
function containerComponent(){
let c = document.createElement('div')
c.className = 'feedback__container'
let form = formComponent()
c.appendChild(form)
return(c)
}
function formComponent(){
let form = document.createElement('form')
form.className = 'feedback__form'
form.autocomplete = 'false'
form.addEventListener('submit', sendFeedback)
let introText = document.createElement('p')
introText.className = 'feedback__intro'
introText.textContent = 'Found a bug? Please tell me about it, I\'ll fix it!'
form.appendChild(introText)
let nameInput = document.createElement('input')
nameInput.className = 'feedback__input feedback__name'
nameInput.placeholder = 'Name'
nameInput.type = 'text'
nameInput.name = 'name'
nameInput.addEventListener('keyup', checkForm)
nameInput.addEventListener('blur', checkElement)
form.appendChild(nameInput)
let emailInput = document.createElement('input')
emailInput.className = 'feedback__input feedback__email'
emailInput.placeholder = 'Email'
emailInput.type = 'email'
emailInput.name = 'email'
emailInput.addEventListener('keyup', checkForm)
emailInput.addEventListener('blur', checkElement)
form.appendChild(emailInput)
let contentInput = document.createElement('textarea')
contentInput.className = 'feedback__input feedback__content'
contentInput.placeholder = 'Write your message'
contentInput.name = 'content'
contentInput.autocomplete = 'false'
contentInput.addEventListener('keyup', checkForm)
contentInput.addEventListener('blur', checkElement)
form.appendChild(contentInput)
let submitButton = document.createElement('button')
submitButton.className = 'feedback__submit js-feedback__submit'
submitButton.textContent = 'Send'
submitButton.type = 'submit'
submitButton.disabled = true
form.appendChild(submitButton)
let submitLoader = document.createElement('div')
submitLoader.className = 'feedback__loader js-feedback__loader feedback__loader--none'
for(let i = 0; i < 4; i++){
let dot = document.createElement('div')
submitLoader.appendChild(dot)
}
form.appendChild(submitLoader)
return(form)
}
// this component is called when the xhr request is done and successful
function successComponent(){
let successText = document.createElement('p')
successText.className = 'feedback__success'
successText.textContent = 'Thank you 🙌🙌 I\'ll contact you if I have any questions!'
return(successText)
}
// this component is called when the xhr request is done with an error :( - it had an error below the send button
function errorComponent(){
let errorText = document.createElement('p')
errorText.className = 'feedback__error'
errorText.textContent = 'Its seems there was a problem sending your feedback 😳 Please try again 🙏'
return(errorText)
}
// the toggle open or close the box acording to the state.
//If the box is closed after the form being sent, i reset the form inside so another form can be sent
function toggle(e){
if( state.active){
e.parentNode.classList.remove('active')
if(state.sent){
let feedbackBox = document.getElementById('feedback')
let formContainer = feedbackBox.querySelector('.feedback__container')
while (formContainer.firstChild) {
formContainer.removeChild(formContainer.firstChild);
}
let form = formComponent()
formContainer.appendChild(form)
state.sent = !state.sent
}
} else {
e.parentNode.classList.add('active')
}
state.active = !state.active
// This was to get info about the browser and on which page he was.
// Well, i forgot to implement this in this version, i'll update later
console.log(navigator.userAgent)
console.log(window.location.href)
}
// this is binded to input on... change i think?
function checkForm(e){
this.classList.remove('error')
let form = this.parentNode
validateForm(form)
}
// this is binded to input on... blur..
function checkElement(e){
let form = this.parentNode
validateElement(this, (error)=>{
addError(this, error)
})
validateForm(form)
}
// this is a basic check of the field > is there content ? is the email valid ? k nice
function validateElement(el, callback){
let type
let error = null
let value = el.value
if( el.tagName == 'textarea'){
type = 'text'
} else {
type = el.type
}
if(value.length == 0 ){
error = 'empty'
}
if(type == 'email'){
if(!validateEmail(value)){
error = 'invalid'
}
}
callback(error)
}
function validateEmail(email) {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
// the error is pretty basic, i just add a class error so the field is RED
function addError(el, error){
if(error){
el.classList.add('error')
}
}
//the validate form thingy check if the overall form is OK. IF yes, the send button is enabled
function validateForm(el){
let els = el.elements;
let errors = []
for (let i=0; i<els.length; i++){
if( els[i].tagName == 'INPUT' || els[i].tagName == 'TEXTAREA'){
validateElement(els[i], function(error){
if(error){
errors.push(error)
}
})
}
}
let button = el.querySelector('.feedback__submit')
if(errors.length == 0){
button.disabled = false;
} else {
button.disabled = true;
}
}
//here is function that send the form on submit. it sends to my backend at the address /v1/feedback, so you'd want to change that
function sendFeedback(e){
e.preventDefault()
let button = document.querySelector('.js-feedback__submit')
button.classList.add('feedback__submit--none')
let loader = document.querySelector('.js-feedback__loader')
loader.classList.remove('feedback__loader--none')
var formData = new FormData(this);
for (var pair of formData.entries()) {
console.log(pair[0]+ ', ' + pair[1]);
}
let request = new XMLHttpRequest()
request.onreadystatechange = ()=> {
if (request.readyState === 4 && request.status >= 200 && request.status <=299) {
state.sent = true
showSuccess()
} else if(request.readyState === 4) {
showError()
}
}
//here change the url
request.open('POST', '/v1/feedback', true)
request.send(formData)
}
//this is to show the error component when the request is NOT A SUCCESS
function showError(){
let feedbackBox = document.getElementById('feedback')
feedbackBox.classList.add('error')
let formContainer = feedbackBox.querySelector('.feedback__container')
if(!formContainer.querySelector('.feedback__error')){
let error = errorComponent()
formContainer.appendChild(error)
}
let button = formContainer.querySelector('.js-feedback__submit')
button.classList.remove('feedback__submit--none')
let loader = formContainer.querySelector('.js-feedback__loader')
loader.classList.add('feedback__loader--none')
}
//Mister success
function showSuccess(){
let feedbackBox = document.getElementById('feedback')
let formContainer = feedbackBox.querySelector('.feedback__container')
if(formContainer.querySelector('.feedback__error')){
feedbackBox.classList.remove('error')
}
while (formContainer.firstChild) {
formContainer.removeChild(formContainer.firstChild);
}
let success = successComponent()
formContainer.appendChild(success)
}
}
export default feedback;
//so on post at /v1/feedback, i call this function get here, which get the content of the form
const bot = require('./BotController').bot
const get = async function (req, res) {
res.setHeader('Content-Type', 'application/json')
let feedback_info = req.body
console.log(req.body)
let message = req.body.name +' sent you feedback: \n'+req.body.email+'\n'+ req.body.content
// I send the message to the chat here > I need the ID of the chat between my and my bot.
// I send a first message and got the ID, and i put it in my.env file
// Maybe not the best idk
bot.telegram.sendMessage( process.env.CHAT_ID, message, {parse_mode : 'Markdown'})
return ReS(res, {}, 200)
}
module.exports.get = get
//BACKEND : I have a router in an express app, so here is where i get the xhr post
const express = require('express')
const router = express.Router()
const multer = require('multer')
const FeedbackController = require('../controllers/FeedbackController')
router.post('/v1/feedback', multer().none(), FeedbackController.get )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment