simplegoals
Version:
Motivate your users with pleasant and easy-to-use achievements.
622 lines (540 loc) • 23.5 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define("SimpleGoals", [], factory);
else if(typeof exports === 'object')
exports["SimpleGoals"] = factory();
else
root["SimpleGoals"] = factory();
})(window, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./src/index.css":
/*!***********************!*\
!*** ./src/index.css ***!
\***********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
// extracted by mini-css-extract-plugin
/***/ }),
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! exports provided: init, unlock, showOverview, default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "init", function() { return init; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "unlock", function() { return unlock; });
/* harmony import */ var _modules_local_goals__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./modules/local-goals */ "./src/modules/local-goals.js");
/* harmony import */ var _modules_cloud_storage__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./modules/cloud-storage */ "./src/modules/cloud-storage.js");
/* harmony import */ var _modules_overview__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./modules/overview */ "./src/modules/overview.js");
/* harmony import */ var _modules_achievements__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./modules/achievements */ "./src/modules/achievements.js");
/* harmony import */ var _modules_buttons__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./modules/buttons */ "./src/modules/buttons.js");
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "showOverview", function() { return _modules_overview__WEBPACK_IMPORTED_MODULE_2__["showOverview"]; });
__webpack_require__ (/*! ./index.css */ "./src/index.css")
let options = {
timeout: 0,
onGoalUnlock: (goalKey) => {},
freshStart: false,
useCloudStorage: false,
appId: null,
user: {}
}
let goals = null
const init = async config => {
Object.assign(options, config)
goals = Object(_modules_local_goals__WEBPACK_IMPORTED_MODULE_0__["prepareGoals"])(options)
await Object(_modules_cloud_storage__WEBPACK_IMPORTED_MODULE_1__["loadFromCloud"])(goals, options)
Object(_modules_overview__WEBPACK_IMPORTED_MODULE_2__["initOverview"])(goals, options)
Object(_modules_buttons__WEBPACK_IMPORTED_MODULE_4__["initButtons"])(goals, options)
}
const unlock = async (name) => {
Object(_modules_achievements__WEBPACK_IMPORTED_MODULE_3__["unlockGoal"])(name, goals, options)
}
/* harmony default export */ __webpack_exports__["default"] = ({init, unlock, showOverview: _modules_overview__WEBPACK_IMPORTED_MODULE_2__["showOverview"]});
/***/ }),
/***/ "./src/modules/achievements.js":
/*!*************************************!*\
!*** ./src/modules/achievements.js ***!
\*************************************/
/*! exports provided: unlockGoal */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "unlockGoal", function() { return unlockGoal; });
/* harmony import */ var _support__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./support */ "./src/modules/support.js");
/* harmony import */ var _local_goals__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./local-goals */ "./src/modules/local-goals.js");
/* harmony import */ var _cloud_storage__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./cloud-storage */ "./src/modules/cloud-storage.js");
/* harmony import */ var _overview__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./overview */ "./src/modules/overview.js");
/* harmony import */ var _styles__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./styles */ "./src/modules/styles.js");
const createAchievementHtml = (key, goal, button, options) => {
const achievementHtmlString = `
<div class="simplegoals-achievement" id="simplegoals-achievement-${key}">
<div class="simplegoals-achievement__icon">
<img src="https://static.simplegoals.co/cup.svg" />
</div>
<div class="simplegoals-achievement__body">
<p class="simplegoals-achievement__notification">
New achievement unlocked
</p>
<p class="simplegoals-achievement__name" id="simplegoals-achievement__name">${goal.name}</p>
</div>
<div class="simplegoals-achievement__button" id="simplegoals-achievement__button-${key}" role="button">${button}</div>
<button class="simplegoals-achievement__close-button" id="simplegoals-achievement__close-button-${key}">
<svg viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M50 50L450 450" stroke-width="100" stroke-linecap="round"/>
<path d="M50 450L450 50" stroke-width="100" stroke-linecap="round"/>
</g>
</svg>
</button>
</div>
`
const node = new DOMParser().parseFromString(achievementHtmlString , 'text/html').body.firstChild
Object(_styles__WEBPACK_IMPORTED_MODULE_4__["applyStyles"])(node, options)
document.body.appendChild(node)
return node
}
const recalculateAchievementsTop = () => {
const nodes = document.getElementsByClassName('simplegoals-achievement')
for(let i=0; i < nodes.length; i++) {
nodes[i].style.top = `${1 + i*7}em`
}
}
const showAchievement = (name, goal, options) => {
const key = Object(_support__WEBPACK_IMPORTED_MODULE_0__["hashCode"])(name)
const achievement = createAchievementHtml(key, goal, 'Learn more', options)
recalculateAchievementsTop()
achievement.classList.add('simplegoals-achievement--opened')
initAchievement(key, achievement)
if (options.timeout) {
achievementTimeout = setTimeout(() => hideAchievement(achievement), options.timeout)
}
}
const initAchievement = (key, achievement) => {
achievement.addEventListener('click', event => achievement.classList.toggle('simplegoals-achievement--clicked'))
const trigger = document.getElementById(`simplegoals-achievement__close-button-${key}`)
trigger.addEventListener('click', event => hideAchievement(achievement))
const overviewTrigger = document.getElementById(`simplegoals-achievement__button-${key}`)
overviewTrigger.addEventListener('click', event => {
hideAchievement(achievement)
setTimeout(_overview__WEBPACK_IMPORTED_MODULE_3__["showOverview"], 200)
})
}
const hideAchievement = (achievement) => {
achievement.classList.remove('simplegoals-achievement--opened')
achievement.classList.add('simplegoals-achievement--closed')
setTimeout(() => {
achievement.parentNode.removeChild(achievement)
recalculateAchievementsTop()
}, 2000)
}
const unlockGoal = async (name, goals, options) => {
if(!goals || !goals[name]) { return }
if(goals[name].unlocked){
return
}
goals[name].unlocked = true
options.onGoalUnlock(name)
Object(_local_goals__WEBPACK_IMPORTED_MODULE_1__["saveLocalGoal"])(name, options)
await Object(_cloud_storage__WEBPACK_IMPORTED_MODULE_2__["saveCloudGoal"])(name, options)
showAchievement(name, goals[name], options)
Object(_overview__WEBPACK_IMPORTED_MODULE_3__["rerenderOverviewGoals"])(goals)
}
/***/ }),
/***/ "./src/modules/buttons.js":
/*!********************************!*\
!*** ./src/modules/buttons.js ***!
\********************************/
/*! exports provided: initButtons */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "initButtons", function() { return initButtons; });
/* harmony import */ var _overview__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./overview */ "./src/modules/overview.js");
/* harmony import */ var _achievements__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./achievements */ "./src/modules/achievements.js");
const initButtons = (goals, options) => {
document.addEventListener('click', function (event) {
if (event.target.dataset.simplegoalsUnlock) {
event.preventDefault()
Object(_achievements__WEBPACK_IMPORTED_MODULE_1__["unlockGoal"])(event.target.dataset.simplegoalsUnlock, goals, options)
}
if (event.target.dataset.simplegoalsOverview) {
event.preventDefault()
Object(_overview__WEBPACK_IMPORTED_MODULE_0__["showOverview"])();
}
}, false);
}
/***/ }),
/***/ "./src/modules/cloud-storage.js":
/*!**************************************!*\
!*** ./src/modules/cloud-storage.js ***!
\**************************************/
/*! exports provided: saveCloudGoal, loadFromCloud */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "saveCloudGoal", function() { return saveCloudGoal; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "loadFromCloud", function() { return loadFromCloud; });
/* harmony import */ var _support__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./support */ "./src/modules/support.js");
const saveCloudGoal = async (name, options) => {
if (options.useCloudStorage) {
let data = {
name: name
}
addAuthData(data, options)
await Object(_support__WEBPACK_IMPORTED_MODULE_0__["postRequest"])("/goals/unlock", data)
} else {
return false
}
}
const addAuthData = (hash, options) => {
hash.appId = options.appId
hash.user = options.user
}
const loadFromCloud = async (goals, options) => {
if (options.useCloudStorage) {
let data = {
goals: Object.keys(goals).map((key) => {
return {
name: key,
description: goals[key].description
}
}),
unlockedGoals: Object.keys(goals).filter((key) => {
goals[key].unlocked
})
}
addAuthData(data, options)
const response = await Object(_support__WEBPACK_IMPORTED_MODULE_0__["postRequest"])("/sessions", data)
if (response) {
updateGoalsFromCloud(response, goals)
}
} else {
return false
}
}
const updateGoalsFromCloud = (response, goals) => {
for (const key of Object.keys(goals)) {
goals[key].unlocked = response.unlockedGoals.includes(key)
}
}
/***/ }),
/***/ "./src/modules/local-goals.js":
/*!************************************!*\
!*** ./src/modules/local-goals.js ***!
\************************************/
/*! exports provided: saveLocalGoal, prepareGoals */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "saveLocalGoal", function() { return saveLocalGoal; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "prepareGoals", function() { return prepareGoals; });
const saveLocalGoal = (name, options) => {
if (typeof(Storage) !== "undefined" && !options.freshStart) {
let localGoals = getLocalGoals(options)
localGoals.push(name)
localStorage.setItem('simplegoals-storage', JSON.stringify(localGoals))
}
}
const getLocalGoals = (options) => {
if (typeof(Storage) !== "undefined" && !options.freshStart) {
return JSON.parse(localStorage.getItem('simplegoals-storage')) || []
} else {
return []
}
}
const prepareGoals = (options) => {
let goals = options.goals
const localGoals = getLocalGoals(options)
for (const key of Object.keys(goals)) {
goals[key].unlocked = localGoals.includes(key)
}
return goals
}
/***/ }),
/***/ "./src/modules/overview.js":
/*!*********************************!*\
!*** ./src/modules/overview.js ***!
\*********************************/
/*! exports provided: initOverview, rerenderOverviewGoals, showOverview */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "initOverview", function() { return initOverview; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "rerenderOverviewGoals", function() { return rerenderOverviewGoals; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "showOverview", function() { return showOverview; });
/* harmony import */ var _styles__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./styles */ "./src/modules/styles.js");
let overview = null
const initOverview = (goals, options) => {
createOverviewHtml(goals, options)
overview = document.getElementById('simplegoals-overview')
const trigger = document.getElementById('simplegoals-overview__close-button')
trigger.addEventListener('click', event => hideOverview())
}
const createOverviewHtml = (goals, options) => {
const overviewHtmlString = `
<div class="simplegoals-overview" id="simplegoals-overview">
<div class="simplegoals-overview__wrapper">
<div class="simplegoals-overview__body">
<h1 class="simplegoals-overview__title">Achievements</h1>
<p class="simplegoals-overview__subtitle">Complete tasks and unlock achievements</p>
<div class="simplegoals-overview__goals" id="simplegoals-overview__goals">
` + overviewGoalsHtml(goals) + `
</div>
<button class="simplegoals-overview__close-button" id="simplegoals-overview__close-button">
Close
</button>
<div class="simplegoals-overview__attribution">
Powered by <a class="simplegoals-overview__attribution-link" href="https://simplegoals.co" target="_blank">SimpleGoals</a>
</div>
</div>
</div>
</div>
`
const node = new DOMParser().parseFromString(overviewHtmlString , 'text/html').body.firstChild
Object(_styles__WEBPACK_IMPORTED_MODULE_0__["applyStyles"])(node, options)
document.body.appendChild(node)
}
const overviewGoalsHtml = (goals) => {
let result = "";
const sortedKeys = Object.keys(goals).sort((a, b) => goals[b].unlocked - goals[a].unlocked)
for (const key of sortedKeys) {
result += `
<div class="simplegoals-overview-goal ${goals[key].unlocked ? "" : "simplegoals-overview-goal--locked"}">
<div class="simplegoals-overview-goal__icon">
<img src="https://static.simplegoals.co/cup.svg" />
</div>
<div class="simplegoals-overview-goal__body">
<h3 class="simplegoals-overview-goal__name">${goals[key].name}</h3>
<p class="simplegoals-overview-goal__description">
${goals[key].description}
</p>
</div>
</div>
`
}
return result
}
const rerenderOverviewGoals = (goals) => {
document.getElementById('simplegoals-overview__goals').innerHTML = overviewGoalsHtml(goals)
}
const showOverview = () => {
if(!overview) { return }
overview.scrollTo(0, 0)
overview.classList.add('simplegoals-overview--opened')
}
const hideOverview = () => {
overview.classList.remove('simplegoals-overview--opened')
}
/***/ }),
/***/ "./src/modules/styles.js":
/*!*******************************!*\
!*** ./src/modules/styles.js ***!
\*******************************/
/*! exports provided: applyStyles */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "applyStyles", function() { return applyStyles; });
const defaultStyleOptions = {
fontFamily: 'BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif',
background: '#ffffff',
title: '#000000',
subtitle: '#718096',
primary: '#38b2ac',
primaryHover: '#319795',
opposite: '#ffffff'
}
const getStyles = (options = {}) => {
let styles = defaultStyleOptions;
['fontFamily', 'background', 'title', 'subtitle'].forEach(key => {
if (options[key]) {
styles[key] = options[key]
}
})
if (options.primary) {
styles.primary = options.primary
styles.primaryHover = getHover(options.primary)
styles.opposite = getOpposite(options.primary)
}
['primaryHover', 'opposite'].forEach(key => {
if (options[key]) {
styles[key] = options[key]
}
})
return styles
}
const getHover = (color) => {
const percent = 7
let R = parseInt(color.substring(1,3),16);
let G = parseInt(color.substring(3,5),16);
let B = parseInt(color.substring(5,7),16);
R = parseInt(R * (100 - percent) / 100);
G = parseInt(G * (100 - percent) / 100);
B = parseInt(B * (100 - percent) / 100);
R = (R<255)?R:255;
G = (G<255)?G:255;
B = (B<255)?B:255;
const RR = ((R.toString(16).length==1)?"0"+R.toString(16):R.toString(16));
const GG = ((G.toString(16).length==1)?"0"+G.toString(16):G.toString(16));
const BB = ((B.toString(16).length==1)?"0"+B.toString(16):B.toString(16));
return "#"+RR+GG+BB;
}
const getOpposite = (hex) => {
if (hex.indexOf('#') === 0) {
hex = hex.slice(1);
}
// convert 3-digit hex to 6-digits.
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
if (hex.length !== 6) {
throw new Error('Invalid HEX color.');
}
var r = parseInt(hex.slice(0, 2), 16),
g = parseInt(hex.slice(2, 4), 16),
b = parseInt(hex.slice(4, 6), 16);
// http://stackoverflow.com/a/3943023/112731
return (r * 0.299 + g * 0.587 + b * 0.114) > 186
? '#000000'
: '#FFFFFF';
}
const applyStyles = (element, options) => {
let styles = getStyles(options.theme)
element.style.setProperty('--simplegoals-font-family', styles.fontFamily);
element.style.setProperty('--simplegoals-color-background', styles.background);
element.style.setProperty('--simplegoals-color-title', styles.title);
element.style.setProperty('--simplegoals-color-subtitle', styles.subtitle);
element.style.setProperty('--simplegoals-color-primary', styles.primary);
element.style.setProperty('--simplegoals-color-primary-hover', styles.primaryHover);
element.style.setProperty('--simplegoals-color-opposite', styles.opposite);
}
/***/ }),
/***/ "./src/modules/support.js":
/*!********************************!*\
!*** ./src/modules/support.js ***!
\********************************/
/*! exports provided: postRequest, hashCode */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "postRequest", function() { return postRequest; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "hashCode", function() { return hashCode; });
const apiUrl = "https://api.simplegoals.co"
const postRequest = async (endpoint, data) => {
const response = await fetch(`${apiUrl}${endpoint}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
})
if(response.ok) {
return await response.json()
} else {
return false
}
}
const hashCode = (source) => {
let hash = 0;
if (source.length == 0) { return hash; }
for (let i = 0; i < source.length; i++) {
let char = source.charCodeAt(i);
hash = ((hash<<5)-hash)+char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
/***/ })
/******/ });
});
//# sourceMappingURL=simplegoals.js.map