Created
January 4, 2020 14:27
-
-
Save maxkostinevich/75a6f224ae7f03763e202d01f0e57e1a to your computer and use it in GitHub Desktop.
Serverless Social Proof widget for Shopify
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
<!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> |
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
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>' | |
}; |
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
/* | |
* 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