Last active
June 15, 2018 18:15
-
-
Save marcantoine/1c9747b1f6807eb8b409b6406c2f962b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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