Skip to content

Instantly share code, notes, and snippets.

@maxkostinevich
Created January 4, 2020 14:27
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save maxkostinevich/75a6f224ae7f03763e202d01f0e57e1a to your computer and use it in GitHub Desktop.
Save maxkostinevich/75a6f224ae7f03763e202d01f0e57e1a to your computer and use it in GitHub Desktop.
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