Serverless Social Proof widget for Shopify
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> | |
<title>Static Template</title> | |
</head> | |
<body> | |
<h1> | |
This is a static template, there is no bundler or bundling involved! | |
</h1> | |
<script src="//unpkg.com/axios/dist/axios.min.js"></script> | |
<script src="widget.js" type="text/javascript"></script> | |
<script type="text/javascript"> | |
// Make a request for a user with a given ID | |
axios | |
.get("https://fomo-demo.frontier.workers.dev/") | |
.then(function(response) { | |
// Widget data | |
fomowidget.data = response.data.message; | |
// Init the widget | |
fomowidget.init(); | |
}) | |
.catch(function(error) { | |
// handle error | |
console.log(error); | |
}); | |
</script> | |
</body> | |
</html> |
var fomowidget = { | |
widgetContainer: "", | |
widgetInnerContent: "", | |
closeButton: "", | |
loop_index: 0, | |
init: function() { | |
var self = this; | |
// Hide widgets on mobile devices | |
if (self.settings.hideOnMobile && self.isMobile()) { | |
return true; | |
} | |
self.shuffleArray(self.data); | |
self.appendWidget(); | |
self.eventClose(); | |
setTimeout(function() { | |
self.rotateWidget(); | |
}, self.settings.intialDelay); | |
}, | |
// Load widget CSS and HTML | |
appendWidget: function() { | |
var self = this; | |
// CSS | |
var css = document.createElement("style"); | |
css.innerHTML = self.settings.widgetCss; | |
document.body.appendChild(css); | |
// HTML | |
document.body.innerHTML += self.settings.widgetHtml; | |
// Get DOM elements | |
self.widgetContainer = document.getElementById("cp-purchase-notification"); | |
self.widgetInnerContent = document.getElementById("cp-widget-inner"); | |
self.closeButton = document.getElementById("cp-widget-close"); | |
}, | |
// Show widget | |
showWidget: function() { | |
var self = this; | |
self.widgetContainer.classList.remove("fade-out"); | |
self.widgetContainer.classList.add("fade-in"); | |
self.widgetContainer.style.display = "block"; | |
}, | |
// Hide widget | |
hideWidget: function() { | |
var self = this; | |
self.widgetContainer.classList.remove("fade-in"); | |
self.widgetContainer.classList.add("fade-out"); | |
}, | |
// Rotate widget content | |
rotateWidget: function() { | |
var self = this; | |
// update widget content | |
// @TODO: check if item exists | |
var data = self.data[self.loop_index]; | |
// @TODO: check if item prop exists | |
self.widgetInnerContent.innerHTML = `<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDMiIGhlaWdodD0iNDMiIHZpZXdCb3g9IjAgMCA0MyA0MyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjEuNSIgY3k9IjIxLjUiIHI9IjIxLjUiIGZpbGw9IiNFQkUxMDAiIGZpbGwtb3BhY2l0eT0iMC4xNSIvPgo8cGF0aCBkPSJNMjEuNTAwMyA4TDI1LjY3MTUgMTYuODg3N0wzNSAxOC4zMTMzTDI4LjI0OTkgMjUuMjMxMUwyOS44NDMzIDM1TDIxLjUwMDMgMzAuMzg4TDEzLjE1NjcgMzVMMTQuNzUwMSAyNS4yMzExTDggMTguMzEzM0wxNy4zMjg1IDE2Ljg4NzdMMjEuNTAwMyA4WiIgZmlsbD0iI0VGQ0U0QSIvPgo8L3N2Zz4K"/> <p><b>${ | |
data.customer_name | |
}</b> from <b>${data.customer_city}</b> just bought <b>${ | |
data.product_title | |
}</b> <small>A few hours ago</small></p>`; | |
self.showWidget(); | |
// increment loop index | |
self.loop_index++; | |
self.loop_index = | |
self.loop_index >= self.data.length ? 0 : self.loop_index++; | |
// Hide widget by timeout | |
setTimeout(function() { | |
self.hideWidget(); | |
// Schedule next loop | |
setTimeout(function() { | |
self.rotateWidget(); | |
}, self.settings.rotateDelay); | |
}, self.settings.displayLength); | |
}, | |
// Bind close button | |
eventClose: function() { | |
var self = this; | |
self.closeButton.addEventListener("click", function(e) { | |
self.hideWidget(); | |
}); | |
}, | |
// Detect mobile device | |
isMobile: function() { | |
return ( | |
navigator.userAgent.match(/Android/i) || | |
navigator.userAgent.match(/BlackBerry/i) || | |
navigator.userAgent.match(/iPhone|iPad|iPod/i) || | |
navigator.userAgent.match(/Opera Mini/i) || | |
navigator.userAgent.match(/IEMobile/i) || | |
navigator.userAgent.match(/webOS/i) | |
); | |
}, | |
// Shuffle array | |
shuffleArray: function(a) { | |
var j, x, i; | |
for (i = a.length; i; i--) { | |
j = Math.floor(Math.random() * i); | |
x = a[i - 1]; | |
a[i - 1] = a[j]; | |
a[j] = x; | |
} | |
} | |
}; | |
// Widget settings | |
fomowidget.settings = { | |
hideOnMobile: false, | |
intialDelay: 1000, | |
displayLength: 2000, | |
rotateDelay: 4000, | |
widgetCss: | |
"@import url(//fonts.googleapis.com/css?family=Raleway:300,700);#cp-purchase-notification{background:#fff;border:0;display:none;border-radius:0;bottom:20px;left:20px;top:auto!important;right:auto!important;padding:0 25px 0 0;position:fixed;text-align:left;width:auto;z-index:99999;font-family:Raleway,sans-serif;-webkit-box-shadow:0 0 4px 0 rgba(0,0,0,.4);-moz-box-shadow:0 0 4px 0 rgba(0,0,0,.4);box-shadow:0 0 4px 0 rgba(0,0,0,.4)}#cp-purchase-notification img{padding-left:5px;padding-top:15px;float:left;max-height:85px;max-width:120px;width:auto}#cp-purchase-notification p{color:#000;float:left;font-size:13px;margin:0 0 0 13px;width:auto;padding:10px 10px 0 0;line-height:20px}#cp-purchase-notification p a{color:#000;display:block;font-size:15px;font-weight:700}#cp-purchase-notification p a:hover{color:#000}#cp-purchase-notification p small{display:block;font-size:10px;margin-bottom:8px}#cp-purchase-notification #cp-widget-close{cursor:pointer;position:absolute;top:10px;right:10px;opacity:.2;background:url(data:image/svg+xml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhciIgZGF0YS1pY29uPSJ0aW1lcy1jaXJjbGUiIGNsYXNzPSJzdmctaW5saW5lLS1mYSBmYS10aW1lcy1jaXJjbGUgZmEtdy0xNiIgcm9sZT0iaW1nIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik0yNTYgOEMxMTkgOCA4IDExOSA4IDI1NnMxMTEgMjQ4IDI0OCAyNDggMjQ4LTExMSAyNDgtMjQ4UzM5MyA4IDI1NiA4em0wIDQ0OGMtMTEwLjUgMC0yMDAtODkuNS0yMDAtMjAwUzE0NS41IDU2IDI1NiA1NnMyMDAgODkuNSAyMDAgMjAwLTg5LjUgMjAwLTIwMCAyMDB6bTEwMS44LTI2Mi4yTDI5NS42IDI1Nmw2Mi4yIDYyLjJjNC43IDQuNyA0LjcgMTIuMyAwIDE3bC0yMi42IDIyLjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwyNTYgMjk1LjZsLTYyLjIgNjIuMmMtNC43IDQuNy0xMi4zIDQuNy0xNyAwbC0yMi42LTIyLjZjLTQuNy00LjctNC43LTEyLjMgMC0xN2w2Mi4yLTYyLjItNjIuMi02Mi4yYy00LjctNC43LTQuNy0xMi4zIDAtMTdsMjIuNi0yMi42YzQuNy00LjcgMTIuMy00LjcgMTcgMGw2Mi4yIDYyLjIgNjIuMi02Mi4yYzQuNy00LjcgMTIuMy00LjcgMTcgMGwyMi42IDIyLjZjNC43IDQuNyA0LjcgMTIuMyAwIDE3eiI+PC9wYXRoPjwvc3ZnPg==);width:16px;height:16px;background-size:cover;progid:DXImageTransform.Microsoft.AlphaImageLoader(src='//s3.eu-west-2.amazonaws.com/fomowidget-static-assets/close.png',sizingMethod='scale')}#cp-purchase-notification #cp-widget-close:hover{opacity:1}@keyframes nFadeIn{from{opacity:0;transform:translate3d(0,100%,0)}to{opacity:1;transform:none}}#cp-purchase-notification.fade-in{opacity:0;animation-name:nFadeIn;animation-duration:1s;animation-fill-mode:both}@keyframes nFadeOut{from{opacity:1}to{opacity:0;transform:translate3d(0,100%,0);bottom:0}}#cp-purchase-notification.fade-out{opacity:0;animation-name:nFadeOut;animation-duration:1s;animation-fill-mode:both}@media screen and (max-width:767px){@keyframes nFadeIn{from{opacity:0;transform:translate3d(0,100%,0)}to{opacity:1;transform:none}}#cp-purchase-notification.fade-in{opacity:0;animation-name:nFadeIn;animation-duration:1s;animation-fill-mode:both}@keyframes nFadeOut{from{opacity:1}to{opacity:0;transform:translate3d(0,100%,0);bottom:0}}#cp-purchase-notification.fade-out{opacity:0;animation-name:nFadeOut;animation-duration:1s;animation-fill-mode:both}#cp-purchase-notification{top:auto!important;right:auto!important;bottom:0!important;left:0!important;width:100%;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;max-width:auto!important;margin-left:0;height:auto;padding:0;text-align:left;border-radius:0}#cp-purchase-notification img{max-width:20%;max-height:auto;position:relative;left:0px;top:0;margin-left:0;margin-right:0;border-radius:0}#cp-purchase-notification p{font-size:11px;width:70%;float:left;margin:0 0 0 13px;padding:10px 10px 0 0}#cp-purchase-notification p a{font-size:13px;height:auto;width:auto;margin-left:0;float:none;padding:0;margin-top:0}}", | |
widgetHtml: | |
'<div class=customized id=cp-purchase-notification><div id="cp-widget-inner"></div><span id=cp-widget-close></span></div>' | |
}; |
/* | |
* Serverless Social Proof widget for Shopify, hosted on Cloudflare Workers. | |
* | |
* Learn more at https://maxkostinevich.com/blog/serverless-shopify-social-proof-widget | |
* | |
* (c) Max Kostinevich / https://maxkostinevich.com | |
*/ | |
// Script configuration | |
const config = { | |
shopify_app_key: "SHOPIFY_PRIVATE_APP_KEY", | |
shopify_app_password: "SHOPIFY_PRIVATE_APP_PASSWORD", | |
shopify_domain: "YOUR_SHOPIFY_STORE.myshopify.com" // Including '.myshopify.com' | |
}; | |
// -------- | |
// Helper function to return JSON response | |
const JSONResponse = (message, status = 200) => { | |
let headers = { | |
headers: { | |
"content-type": "application/json;charset=UTF-8", | |
"Access-Control-Allow-Origin": "*", | |
"Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS", | |
"Access-Control-Allow-Headers": "Content-Type" | |
}, | |
status: status | |
}; | |
let response = { | |
message: message | |
}; | |
return new Response(JSON.stringify(response), headers); | |
}; | |
addEventListener("fetch", event => { | |
const request = event.request; | |
if (request.method === "OPTIONS") { | |
event.respondWith(handleOptions(request)); | |
} else { | |
event.respondWith(handle(request)); | |
} | |
}); | |
const corsHeaders = { | |
"Access-Control-Allow-Origin": "*", | |
"Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS", | |
"Access-Control-Allow-Headers": "Content-Type" | |
}; | |
function handleOptions(request) { | |
if ( | |
request.headers.get("Origin") !== null && | |
request.headers.get("Access-Control-Request-Method") !== null && | |
request.headers.get("Access-Control-Request-Headers") !== null | |
) { | |
// Handle CORS pre-flight request. | |
return new Response(null, { | |
headers: corsHeaders | |
}); | |
} else { | |
// Handle standard OPTIONS request. | |
return new Response(null, { | |
headers: { | |
Allow: "GET, HEAD, POST, OPTIONS" | |
} | |
}); | |
} | |
} | |
async function handle(request) { | |
const shopify_url = `https://${ | |
config.shopify_domain | |
}/admin/api/2020-01/orders.json`; | |
return fetch(shopify_url, { | |
method: "GET", | |
headers: { | |
"Content-Type": "application/json", | |
Authorization: | |
"Basic " + | |
btoa(`${config.shopify_app_key}:${config.shopify_app_password}`) | |
} | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
let orders = data.orders; | |
let filteredOrderData = []; | |
Object.keys(orders).map(order => { | |
orders[order].line_items.map(item => { | |
// It's important to keep the order's financial information private | |
// Expose only selected information about the purchase, | |
// like customer first name, city and purchased product name | |
filteredOrderData.push({ | |
customer_name: orders[order].shipping_address.first_name, | |
customer_city: orders[order].shipping_address.city, | |
product_title: item.title | |
}); | |
}); | |
}); | |
return filteredOrderData; | |
}) | |
.then(data => { | |
return JSONResponse(data); | |
}) | |
.catch(err => { | |
return JSONResponse("Oops! Something went wrong.", 400); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment