884 lines
25 KiB
JavaScript
884 lines
25 KiB
JavaScript
|
|
|
|
|
|
/**
|
|
* @param {!Element} container
|
|
* @constructor
|
|
*/
|
|
var BabyStats = function(container) {
|
|
var urlRE = new RegExp('^/baby/([-0-9a-f]{36})$');
|
|
var match = window.location.pathname.match(urlRE);
|
|
if (!match) {
|
|
window.location.pathname = '/baby/' + Cosmopolite.uuid();
|
|
return;
|
|
}
|
|
var id = match[1];
|
|
|
|
this.container_ = container;
|
|
|
|
this.tileScaleHeight_ = 1;
|
|
this.tileScaleWidth_ = 1;
|
|
|
|
this.tiles_ = [
|
|
{
|
|
type: 'asleep',
|
|
description: 'Asleep',
|
|
cancels: ['awake'],
|
|
ignore_duplicates: true,
|
|
},
|
|
{
|
|
type: 'awake',
|
|
description: 'Awake',
|
|
cancels: ['asleep'],
|
|
ignore_duplicates: true,
|
|
},
|
|
{
|
|
type: 'diaper_feces',
|
|
description: 'Diaper change\n(feces)',
|
|
implies: ['awake'],
|
|
timeout: 60 * 30,
|
|
},
|
|
{
|
|
type: 'diaper_urine',
|
|
description: 'Diaper change\n(urine only)',
|
|
implies: ['awake'],
|
|
timeout: 60 * 30,
|
|
},
|
|
{
|
|
type: 'feeding_breast',
|
|
description: 'Feeding\n(breast)',
|
|
implies: ['awake'],
|
|
timeout: 60 * 30,
|
|
},
|
|
{
|
|
type: 'feeding_bottle_milk',
|
|
description: 'Feeding\n(bottled breast milk)',
|
|
implies: ['awake'],
|
|
timeout: 60 * 30,
|
|
},
|
|
{
|
|
type: 'feeding_formula',
|
|
description: 'Feeding\n(formula)',
|
|
implies: ['awake'],
|
|
timeout: 60 * 30,
|
|
},
|
|
{
|
|
type: 'bath',
|
|
description: 'Bath',
|
|
implies: ['awake'],
|
|
timeout: 60 * 30,
|
|
},
|
|
{
|
|
type: 'pumped',
|
|
description: 'Breast pumped',
|
|
timeout: 60 * 30,
|
|
},
|
|
];
|
|
|
|
this.intervals_ = {};
|
|
this.displayDates_ = {};
|
|
|
|
this.buildStylesheet_();
|
|
|
|
this.cosmo_ = new Cosmopolite();
|
|
this.cosmo_.addEventListener('login', this.onLogin_.bind(this));
|
|
this.cosmo_.addEventListener('logout', this.onLogout_.bind(this));
|
|
|
|
this.client_id_ = this.cosmo_.uuid();
|
|
hogfather.PublicChat.Join(this.cosmo_, id).then(this.onChatReady_.bind(this));
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {hogfather.PublicChat} chat
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.onChatReady_ = function(chat) {
|
|
this.chat_ = chat;
|
|
|
|
this.buildCells_();
|
|
this.buildLayout_();
|
|
|
|
window.addEventListener('resize', this.rebuildIfNeeded_.bind(this));
|
|
|
|
var grid = this.calculateGrid_();
|
|
this.gridWidthCells_ = grid.gridWidthCells;
|
|
this.gridHeightCells_ = grid.gridHeightCells;
|
|
this.buildGrid_();
|
|
|
|
if (!this.chat_.amWriter()) {
|
|
// Start on back side if we're read-only.
|
|
this.flipperRule_.style.transform = 'rotateY(180deg)';
|
|
}
|
|
|
|
var messages = this.chat_.getMessages();
|
|
messages.forEach(this.handleMessage_.bind(this, false));
|
|
this.chat_.addEventListener('message', this.onMessage_.bind(this));
|
|
this.chat_.addEventListener('request', this.checkOverlay_.bind(this));
|
|
this.chat_.addEventListener('request_denied', this.checkOverlay_.bind(this));
|
|
this.chat_.addEventListener('acl_change', this.checkOverlay_.bind(this));
|
|
|
|
this.updateTileStatus_();
|
|
this.updateDisplayPage_();
|
|
|
|
// Cheap hack to get the DOM to render by yielding before we turn on
|
|
// transitions.
|
|
window.setTimeout(this.setTransitions_.bind(this), 0);
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {Event} e
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.onLogin_ = function(e) {
|
|
this.loginRule_.style.visibility = 'hidden';
|
|
this.checkOverlay_();
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {Event} e
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.onLogout_ = function(e) {
|
|
this.loginURL_ = e.detail.login_url;
|
|
this.loginRule_.style.visibility = 'visible';
|
|
this.checkOverlay_();
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.onLoginClick_ = function() {
|
|
window.open(this.loginURL_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.onFlipClick_ = function() {
|
|
if (this.flipperRule_.style.transform) {
|
|
this.flipperRule_.style.transform = null;
|
|
} else {
|
|
this.flipperRule_.style.transform = 'rotateY(180deg)';
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {Event} e
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.onMessage_ = function(e) {
|
|
this.handleMessage_(true, e.detail);
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {string} type
|
|
* @return {Object}
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.findTile_ = function(type) {
|
|
return this.tiles_.find(function(tile) { return tile.type == type; });
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {boolean} isEvent
|
|
* @param {Cosmopolite.typeMessage} message
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.handleMessage_ = function(isEvent, message) {
|
|
if (message.message.sender_name &&
|
|
!this.yourName_.value &&
|
|
message.sender == this.cosmo_.currentProfile()) {
|
|
this.yourName_.value = message.message.sender_name;
|
|
this.checkOverlay_();
|
|
}
|
|
|
|
switch (message.message.type) {
|
|
case 'child_name_change':
|
|
if (!isEvent || message.message.client_id != this.client_id_) {
|
|
this.childName_.value = message.message.child_name;
|
|
this.checkOverlay_();
|
|
}
|
|
document.title = message.message.child_name;
|
|
this.displayChildName_.textContent = message.message.child_name;
|
|
break;
|
|
|
|
default:
|
|
var tile = this.findTile_(message.message.type);
|
|
if (tile) {
|
|
if (tile.ignore_duplicates && tile.active) {
|
|
// Ignore (double trigger of a state-based tile)
|
|
} else if (tile.active && message.created - tile.lastSeen < 60) {
|
|
// Ignore (too fast repetition)
|
|
} else {
|
|
tile.lastSeen = message.created;
|
|
tile.active = true;
|
|
tile.messages.push(message);
|
|
(tile.cancels || []).forEach(function(type) {
|
|
tile2 = this.findTile_(type);
|
|
tile2.active = false;
|
|
}.bind(this));
|
|
this.updateTileStatus_();
|
|
this.updateDisplayPage_();
|
|
this.updateDisplayDate_(message);
|
|
}
|
|
} else {
|
|
console.log('Unknown message type:', message);
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Add a CSS class to a node if it doesn't already have it.
|
|
* @param {!Node} node Node object to add class to
|
|
* @param {!string} className Name of class to add
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.addCSSClass_ = function(node, className) {
|
|
var classes = node.className.split(' ').filter(function(className) {
|
|
return className;
|
|
});
|
|
if (classes.indexOf(className) != -1) {
|
|
// Already has class.
|
|
return;
|
|
}
|
|
classes.push(className);
|
|
node.className = classes.join(' ');
|
|
};
|
|
|
|
|
|
/**
|
|
* Remove a CSS class to a node if it has it.
|
|
* @param {!Node} node Node object to remove class from
|
|
* @param {!string} className Name of class to remove
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.removeCSSClass_ = function(node, className) {
|
|
var classes = node.className.split(' ').filter(function(className) {
|
|
return className;
|
|
});
|
|
var i = classes.indexOf(className);
|
|
if (i == -1) {
|
|
// Already doesn't have class.
|
|
return;
|
|
}
|
|
delete classes[i];
|
|
node.className = classes.join(' ');
|
|
};
|
|
|
|
|
|
/**
|
|
* Check if we need to rebuild the grid layout because of optimal layout
|
|
* changes.
|
|
* @param {Event} e
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.rebuildIfNeeded_ = function(e) {
|
|
var grid = this.calculateGrid_();
|
|
if (this.gridWidthCells_ != grid.gridWidthCells ||
|
|
this.gridHeightCells_ != grid.gridHeightCells) {
|
|
this.gridWidthCells_ = grid.gridWidthCells;
|
|
this.gridHeightCells_ = grid.gridHeightCells;
|
|
this.buildGrid_();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Only set transitions once loaded.
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.setTransitions_ = function() {
|
|
this.gridOverlayRule_.style.transition = '0.4s';
|
|
this.cellOverlayRule_.style.transition = '0.4s';
|
|
this.flipperRule_.style.transition = '1.0s';
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @param {Element} stylesheet
|
|
* @param {string} selector
|
|
* @return {CSSRule}
|
|
*/
|
|
BabyStats.prototype.addStyle_ = function(stylesheet, selector) {
|
|
stylesheet.sheet.insertRule(selector + ' {}', 0);
|
|
return stylesheet.sheet.cssRules[0];
|
|
};
|
|
|
|
|
|
/**
|
|
* Construct our stylesheet and insert it into the DOM.
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.buildStylesheet_ = function() {
|
|
var style = document.createElement('style');
|
|
document.head.appendChild(style);
|
|
|
|
this.flipperRule_ = this.addStyle_(style, 'babyStatsFlipper');
|
|
this.loginRule_ = this.addStyle_(style, '.babyStatsLogin');
|
|
this.gridOverlayRule_ = this.addStyle_(style, 'babyStatsGridOverlay');
|
|
this.rowRule_ = this.addStyle_(style, 'babyStatsRow');
|
|
this.cellRule_ = this.addStyle_(style, 'babyStatsCell');
|
|
this.cellOverlayRule_ = this.addStyle_(style, 'babyStatsCellOverlay');
|
|
};
|
|
|
|
|
|
/**
|
|
* Construct babyStateCell elements for insertion into the DOM.
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.buildCells_ = function() {
|
|
this.cells_ = [];
|
|
this.tiles_.forEach(function(tile) {
|
|
tile.active = false;
|
|
tile.messages = [];
|
|
|
|
var cell = document.createElement('babyStatsCell');
|
|
this.cells_.push(cell);
|
|
|
|
var contents = document.createElement('babyStatsCellContents');
|
|
contents.textContent = tile.description;
|
|
cell.appendChild(contents);
|
|
|
|
tile.statusBox = document.createElement('babyStatsCellStatus');
|
|
cell.appendChild(tile.statusBox);
|
|
|
|
var overlay = document.createElement('babyStatsCellOverlay');
|
|
cell.appendChild(overlay);
|
|
|
|
cell.addEventListener('click', this.onClick_.bind(this, tile, overlay));
|
|
}, this);
|
|
window.setInterval(this.updateTileStatus_.bind(this), 60 * 1000);
|
|
window.setInterval(this.updateDisplayPage_.bind(this), 60 * 1000);
|
|
};
|
|
|
|
|
|
/**
|
|
* Handle a click event on a button.
|
|
* @param {Object} tile tile description struct
|
|
* @param {Element} overlay element to make visible with countdown timer
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.onClick_ = function(tile, overlay) {
|
|
if (this.intervals_[tile.type]) {
|
|
window.clearInterval(this.intervals_[tile.type]);
|
|
delete this.intervals_[tile.type];
|
|
overlay.style.opacity = 0.0;
|
|
return;
|
|
}
|
|
var timer = 5;
|
|
overlay.textContent = timer;
|
|
overlay.style.opacity = 0.5;
|
|
this.intervals_[tile.type] = window.setInterval(function() {
|
|
timer--;
|
|
switch (timer) {
|
|
case 0:
|
|
var types = tile.implies || [];
|
|
types.push(tile.type);
|
|
types.forEach(function(type) {
|
|
this.chat_.sendMessage({
|
|
type: type,
|
|
sender_name: this.yourName_.value,
|
|
});
|
|
}.bind(this));
|
|
overlay.textContent = '✓';
|
|
break;
|
|
|
|
case -1:
|
|
break;
|
|
|
|
case -2:
|
|
window.clearInterval(this.intervals_[tile.type]);
|
|
delete this.intervals_[tile.type];
|
|
overlay.style.opacity = 0.0;
|
|
break;
|
|
|
|
default:
|
|
overlay.textContent = timer;
|
|
break;
|
|
}
|
|
}.bind(this), 1000);
|
|
};
|
|
|
|
|
|
/**
|
|
* Calculate optimal grid sizing.
|
|
* This pile of magic math calculates the optimal grid width and height to
|
|
* maximize the size of all buttons while preserving their aspect ratio.
|
|
* @return {{
|
|
* gridWidthCells: number,
|
|
* gridHeightCells: number,
|
|
* cellWidthPx: number,
|
|
* cellHeightPx: number
|
|
* }}
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.calculateGrid_ = function() {
|
|
var containerWidth = this.gridContainer_.offsetWidth;
|
|
var containerHeight = this.gridContainer_.offsetHeight;
|
|
var numTiles = this.tiles_.length;
|
|
|
|
var heightFactor = containerHeight / this.tileScaleHeight_;
|
|
var widthFactor = containerWidth / this.tileScaleWidth_;
|
|
|
|
var scaleFactor = heightFactor / widthFactor;
|
|
|
|
var gridHeight = Math.sqrt(scaleFactor * numTiles);
|
|
var gridWidth = Math.sqrt(numTiles / scaleFactor);
|
|
|
|
var gridOptions = [
|
|
[Math.ceil(gridWidth), Math.floor(gridHeight)],
|
|
[Math.floor(gridWidth), Math.ceil(gridHeight)],
|
|
[Math.ceil(gridWidth), Math.ceil(gridHeight)],
|
|
];
|
|
|
|
// Check all possible options.
|
|
// We are optimizing for several dimensions (decreasing priority):
|
|
// 1) Be able to fit all the tiles.
|
|
// 2) Maximum scale for an image in each cell.
|
|
// 3) Minimize number of cells.
|
|
var minCells = Number.MAX_VALUE;
|
|
var maxScale = 0.0;
|
|
var chosenHeight, chosenWidth;
|
|
gridOptions.forEach(function(gridOption) {
|
|
var numCells = gridOption[0] * gridOption[1];
|
|
if (numCells < numTiles) {
|
|
// Can't fit all the tiles in (we've rounded down too far).
|
|
return;
|
|
}
|
|
var widthScale = (containerWidth / gridOption[0]) / this.tileScaleWidth_;
|
|
var heightScale = (containerHeight / gridOption[1]) / this.tileScaleHeight_;
|
|
var scale;
|
|
if (widthScale < heightScale) {
|
|
scale = widthScale;
|
|
} else {
|
|
scale = heightScale;
|
|
}
|
|
if (scale < maxScale) {
|
|
// This would make cells smaller than another viable solution.
|
|
return;
|
|
}
|
|
if (scale == maxScale && numCells > minCells) {
|
|
// Same cell size as another viable solution, but ours has more cells.
|
|
return;
|
|
}
|
|
chosenWidth = gridOption[0];
|
|
chosenHeight = gridOption[1];
|
|
minCells = numCells;
|
|
maxScale = scale;
|
|
}, this);
|
|
|
|
return /** @struct */ {
|
|
gridWidthCells: chosenWidth,
|
|
gridHeightCells: chosenHeight,
|
|
cellWidthPx: this.tileScaleWidth_ * maxScale,
|
|
cellHeightPx: this.tileScaleHeight_ * maxScale,
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
* Construct the outer DOM layout.
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.buildLayout_ = function() {
|
|
// Allows loading screen to be embedded in the style tag.
|
|
this.container_.removeAttribute('style');
|
|
|
|
this.addCSSClass_(this.container_, 'babyStatsContainer');
|
|
|
|
var flipper = document.createElement('babyStatsFlipper');
|
|
this.container_.appendChild(flipper);
|
|
|
|
var front = document.createElement('babyStatsFlipperFront');
|
|
flipper.appendChild(front);
|
|
|
|
var back = document.createElement('babyStatsFlipperBack');
|
|
flipper.appendChild(back);
|
|
|
|
// Front (writable) side
|
|
this.childName_ = document.createElement('input');
|
|
this.addCSSClass_(this.childName_, 'babyStatsChildName');
|
|
this.childName_.placeholder = 'Child name';
|
|
this.childName_.addEventListener('input', this.checkOverlay_.bind(this));
|
|
this.childName_.addEventListener('input', this.onChildNameChange_.bind(this));
|
|
front.appendChild(this.childName_);
|
|
|
|
this.yourName_ = document.createElement('input');
|
|
this.addCSSClass_(this.yourName_, 'babyStatsYourName');
|
|
this.yourName_.placeholder = 'Your name';
|
|
this.yourName_.value = localStorage.getItem('babyStats:yourName') || '';
|
|
this.yourName_.addEventListener('input', this.checkOverlay_.bind(this));
|
|
this.yourName_.addEventListener('input', this.onYourNameChange_.bind(this));
|
|
front.appendChild(this.yourName_);
|
|
|
|
var login = document.createElement('img');
|
|
this.addCSSClass_(login, 'babyStatsLogin');
|
|
login.src = '/static/google.svg';
|
|
login.addEventListener('click', this.onLoginClick_.bind(this));
|
|
front.appendChild(login);
|
|
|
|
this.gridContainer_ = document.createElement('babyStatsGridContainer');
|
|
front.appendChild(this.gridContainer_);
|
|
|
|
this.gridOverlay_ = document.createElement('babyStatsGridOverlay');
|
|
front.appendChild(this.gridOverlay_);
|
|
|
|
// Back (read-only) side
|
|
this.displayChildName_ = document.createElement('babyStatsDisplayChildName');
|
|
back.appendChild(this.displayChildName_);
|
|
|
|
this.displaySleepSummary_ =
|
|
document.createElement('babyStatsDisplaySleepSummary');
|
|
back.appendChild(this.displaySleepSummary_);
|
|
this.displaySleepSummary_.appendChild(document.createTextNode('has been '));
|
|
this.displaySleepStatus_ =
|
|
document.createElement('babyStatsDisplaySleepStatus');
|
|
this.displaySleepSummary_.appendChild(this.displaySleepStatus_);
|
|
this.displaySleepSummary_.appendChild(document.createTextNode(' for '));
|
|
this.displaySleepDuration_ =
|
|
document.createElement('babyStatsDisplaySleepDuration');
|
|
this.displaySleepSummary_.appendChild(this.displaySleepDuration_);
|
|
|
|
var displayEventCounts =
|
|
document.createElement('babyStatsDisplayEventCounts');
|
|
back.appendChild(displayEventCounts);
|
|
var eventCountHeader =
|
|
document.createElement('babyStatsDisplayEventCountHeader');
|
|
displayEventCounts.appendChild(eventCountHeader);
|
|
eventCountHeader.appendChild(
|
|
document.createElement('babyStatsDisplayEventCountSpacer'));
|
|
var columns = [
|
|
'Most recent',
|
|
'Past 6h',
|
|
'Past 24h',
|
|
'Past 7d',
|
|
'Past 30d',
|
|
'All time',
|
|
];
|
|
columns.forEach(function(column) {
|
|
var headerCell =
|
|
document.createElement('babyStatsDisplayEventCountHeaderTitle');
|
|
headerCell.textContent = column;
|
|
eventCountHeader.appendChild(headerCell);
|
|
}.bind(this));
|
|
|
|
this.displayEventCountCells_ = {};
|
|
this.tiles_.forEach(function(tile) {
|
|
var group = document.createElement('babyStatsDisplayEventCountGroup');
|
|
displayEventCounts.appendChild(group);
|
|
var groupTitle = document.createElement('babyStatsDisplayEventCountTitle');
|
|
groupTitle.textContent = tile.description;
|
|
group.appendChild(groupTitle);
|
|
|
|
this.displayEventCountCells_[tile.type] = {};
|
|
columns.forEach(function(column) {
|
|
var value = document.createElement('babyStatsDisplayEventCountValue');
|
|
group.appendChild(value);
|
|
this.displayEventCountCells_[tile.type][column] = value;
|
|
}.bind(this));
|
|
}.bind(this));
|
|
|
|
this.displayTimelines_ = document.createElement('babyStatsDisplayTimelines');
|
|
back.appendChild(this.displayTimelines_);
|
|
|
|
var flip = document.createElement('img');
|
|
this.addCSSClass_(flip, 'babyStatsFlip');
|
|
flip.src = '/static/flip.svg';
|
|
flip.addEventListener('click', this.onFlipClick_.bind(this));
|
|
this.container_.appendChild(flip);
|
|
|
|
this.checkOverlay_();
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.requestAccess_ = function() {
|
|
this.chat_.requestAccess(this.yourName_.value);
|
|
};
|
|
|
|
|
|
/**
|
|
* Make the grid overlay visible/hidden based on input field status.
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.checkOverlay_ = function() {
|
|
if (!this.childName_) {
|
|
// buildLayout_() hasn't run yet; not much we can do here.
|
|
return;
|
|
}
|
|
|
|
if (!this.yourName_.value) {
|
|
this.chat_.getMessages().forEach(function(message) {
|
|
if (message.message.sender_name &&
|
|
message.sender == this.cosmo_.currentProfile()) {
|
|
this.yourName_.value = message.message.sender_name;
|
|
}
|
|
}.bind(this));
|
|
}
|
|
|
|
var message = '', actions = [];
|
|
if (!this.childName_.value) {
|
|
message = 'Please enter child name above';
|
|
} else if (!this.yourName_.value) {
|
|
message = 'Please enter your name above';
|
|
} else if (!this.chat_.amWriter()) {
|
|
if (this.chat_.getRequests().some(function(request) {
|
|
return request.sender == this.cosmo_.currentProfile();
|
|
}.bind(this))) {
|
|
message = 'Access request sent.';
|
|
} else {
|
|
message = 'You don\'t have permission to interact with this page.';
|
|
actions.push(['Request Access', this.requestAccess_.bind(this)]);
|
|
}
|
|
} else if (this.chat_.amOwner() && this.chat_.getRequests().length) {
|
|
var request = this.chat_.getRequests()[0];
|
|
message = 'Access request from "' + request.message.info + '"';
|
|
actions.push(['Approve as Owner',
|
|
this.chat_.addOwner.bind(this.chat_, request.sender)]);
|
|
actions.push(['Approve as Contributor',
|
|
this.chat_.addWriter.bind(this.chat_, request.sender)]);
|
|
actions.push(['Deny',
|
|
this.chat_.denyRequest.bind(this.chat_, request.sender)]);
|
|
}
|
|
|
|
if (message) {
|
|
this.gridOverlay_.style.visibility = 'visible';
|
|
this.gridOverlay_.style.opacity = 1.0;
|
|
this.gridOverlay_.innerHTML = '';
|
|
this.gridOverlay_.textContent = message;
|
|
actions.forEach(function(action) {
|
|
var button = document.createElement('babyStatsActionButton');
|
|
button.textContent = action[0];
|
|
button.addEventListener('click', action[1]);
|
|
this.gridOverlay_.appendChild(button);
|
|
}.bind(this));
|
|
} else {
|
|
this.gridOverlay_.style.visibility = 'hidden';
|
|
this.gridOverlay_.style.opacity = 0.0;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.onChildNameChange_ = function() {
|
|
this.chat_.sendMessage({
|
|
type: 'child_name_change',
|
|
child_name: this.childName_.value,
|
|
client_id: this.client_id_,
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Store your name value locally.
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.onYourNameChange_ = function() {
|
|
localStorage.setItem('babyStats:yourName', this.yourName_.value);
|
|
};
|
|
|
|
|
|
/**
|
|
* Construct the grid objects in the DOM.
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.buildGrid_ = function() {
|
|
this.gridContainer_.innerHTML = '';
|
|
|
|
this.rowRule_.style.height = 100 / this.gridHeightCells_ + '%';
|
|
this.cellRule_.style.width = 100 / this.gridWidthCells_ + '%';
|
|
|
|
var i = 0;
|
|
for (var y = 0; y < this.gridHeightCells_; y++) {
|
|
var row = document.createElement('babyStatsRow');
|
|
for (var x = 0; x < this.gridWidthCells_; x++) {
|
|
if (i < this.cells_.length) {
|
|
var cell = this.cells_[i];
|
|
row.appendChild(cell);
|
|
i++;
|
|
}
|
|
}
|
|
this.gridContainer_.appendChild(row);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @param {number} seconds
|
|
* @param {function(number):number=} opt_floatToInt
|
|
* @return {string}
|
|
*/
|
|
BabyStats.prototype.secondsToHuman_ = function(seconds, opt_floatToInt) {
|
|
var floatToInt = opt_floatToInt || Math.floor;
|
|
if (seconds > 60 * 60 * 24 * 2) {
|
|
return floatToInt(seconds / (60 * 60 * 24)).toString() + 'd';
|
|
} else if (seconds > 60 * 60 * 2) {
|
|
return floatToInt(seconds / (60 * 60)).toString() + 'h';
|
|
} else {
|
|
return floatToInt(seconds / 60).toString() + 'm';
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.updateTileStatus_ = function() {
|
|
var now = Date.now() / 1000;
|
|
this.tiles_.forEach(function(tile) {
|
|
if (tile.lastSeen) {
|
|
var timeSince = now - tile.lastSeen;
|
|
tile.statusBox.textContent = (
|
|
timeSince < 60 ?
|
|
'just now' :
|
|
this.secondsToHuman_(timeSince) + ' ago');
|
|
var timedOut = tile.timeout && (now - tile.timeout > tile.lastSeen);
|
|
if (!tile.active || timedOut) {
|
|
this.removeCSSClass_(tile.statusBox, 'babyStatsCellStatusActive');
|
|
} else {
|
|
this.addCSSClass_(tile.statusBox, 'babyStatsCellStatusActive');
|
|
}
|
|
} else {
|
|
tile.statusBox.textContent = 'never';
|
|
this.removeCSSClass_(tile.statusBox, 'babyStatsCellStatusActive');
|
|
}
|
|
}.bind(this));
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
BabyStats.prototype.updateDisplayPage_ = function() {
|
|
var now = Date.now() / 1000;
|
|
|
|
var asleep = this.findTile_('asleep');
|
|
var awake = this.findTile_('awake');
|
|
if (asleep.active || awake.active) {
|
|
this.displaySleepSummary_.style.visibility = 'visible';
|
|
if (asleep.active) {
|
|
this.displaySleepStatus_.textContent = 'asleep';
|
|
var timeSince = now - asleep.lastSeen;
|
|
this.displaySleepDuration_.textContent = this.secondsToHuman_(timeSince);
|
|
} else {
|
|
this.displaySleepStatus_.textContent = 'awake';
|
|
var timeSince = now - awake.lastSeen;
|
|
this.displaySleepDuration_.textContent = this.secondsToHuman_(timeSince);
|
|
}
|
|
} else {
|
|
this.displaySleepSummary_.style.visibility = 'hidden';
|
|
}
|
|
|
|
var cutoffs = [
|
|
['Past 6h', 6 * 60 * 60],
|
|
['Past 24h', 24 * 60 * 60],
|
|
['Past 7d', 7 * 24 * 60 * 60],
|
|
['Past 30d', 30 * 24 * 60 * 60],
|
|
['All time', Number.MAX_VALUE],
|
|
];
|
|
|
|
this.tiles_.forEach(function(tile) {
|
|
if (tile.lastSeen) {
|
|
var timeSince = now - tile.lastSeen;
|
|
this.displayEventCountCells_[tile.type]['Most recent'].textContent = (
|
|
timeSince < 60 ?
|
|
'just now' :
|
|
this.secondsToHuman_(timeSince) + ' ago');
|
|
} else {
|
|
this.displayEventCountCells_[tile.type]['Most recent'].textContent =
|
|
'never';
|
|
}
|
|
|
|
var timestamps = [[], [], [], [], []];
|
|
tile.messages.forEach(function(message) {
|
|
cutoffs.forEach(function(cutoff, i) {
|
|
var timeSince = now - message.created;
|
|
if (timeSince < cutoff[1]) {
|
|
// Sample belongs in this bucket
|
|
timestamps[i].push(message.created);
|
|
}
|
|
}.bind(this));
|
|
}.bind(this));
|
|
|
|
cutoffs.forEach(function(cutoff, i) {
|
|
var text = timestamps[i].length.toString();
|
|
if (timestamps[i].length >= 2) {
|
|
var deltas = [];
|
|
for (var j = 1; j < timestamps[i].length; j++) {
|
|
deltas.push(timestamps[i][j] - timestamps[i][j - 1]);
|
|
}
|
|
deltas.sort();
|
|
var median = deltas[Math.floor(deltas.length / 2)];
|
|
text += '\n⏱ ' + this.secondsToHuman_(median, Math.round);
|
|
}
|
|
this.displayEventCountCells_[tile.type][cutoff[0]].textContent = text;
|
|
}.bind(this));
|
|
}.bind(this));
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @param {Cosmopolite.typeMessage} message
|
|
*/
|
|
BabyStats.prototype.updateDisplayDate_ = function(message) {
|
|
var date = new Date(message.created * 1000);
|
|
var dateStr = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
|
|
var days = [
|
|
'Sunday',
|
|
'Monday',
|
|
'Tuesday',
|
|
'Wednesday',
|
|
'Thursday',
|
|
'Friday',
|
|
'Saturday',
|
|
];
|
|
|
|
if (!this.displayDates_[dateStr]) {
|
|
var dateObj = document.createElement('babyStatsDisplayDate');
|
|
this.displayDates_[dateStr] = dateObj;
|
|
this.displayTimelines_.insertBefore(
|
|
dateObj, this.displayTimelines_.firstChild);
|
|
|
|
var dateTitle = document.createElement('babyStatsDisplayDateTitle');
|
|
dateObj.appendChild(dateTitle);
|
|
dateTitle.textContent =
|
|
date.toLocaleDateString() + ' (' + days[date.getDay()] + ')';
|
|
|
|
var svgns = 'http://www.w3.org/2000/svg';
|
|
var svg = document.createElementNS(svgns, "svg");
|
|
svg.setAttributeNS(null, 'viewBox', '0 0 500 ' + (this.tiles_.length * 10));
|
|
svg.setAttributeNS(null, 'preserveAspectRatio', 'none');
|
|
svg.style.display = 'block';
|
|
svg.style.width = '100%';
|
|
for (var i = 0; i < this.tiles_.length; i++) {
|
|
var line = document.createElementNS(svgns, 'line');
|
|
line.setAttributeNS(null, 'x1', 0);
|
|
line.setAttributeNS(null, 'x2', 500);
|
|
line.setAttributeNS(null, 'y1', i * 10 + 5);
|
|
line.setAttributeNS(null, 'y2', i * 10 + 5);
|
|
line.setAttributeNS(null, 'stroke',
|
|
i % 2 ? 'rgb(233,127,2)' : 'rgb(248,202,0)');
|
|
line.setAttributeNS(null, 'stroke-width', 1);
|
|
svg.appendChild(line);
|
|
}
|
|
dateObj.appendChild(svg);
|
|
}
|
|
};
|