Skip to content

Instantly share code, notes, and snippets.

@loicfrering
Created January 3, 2013 17:24
Show Gist options
  • Save loicfrering/4445123 to your computer and use it in GitHub Desktop.
Save loicfrering/4445123 to your computer and use it in GitHub Desktop.
backbone.datagrid v0.3.2-alpha
// backbone.datagrid v0.3.2-alpha
//
// Copyright (c) 2012 Loïc Frering <[email protected]>
// Distributed under the MIT license
(function() {
var Datagrid = Backbone.View.extend({
initialize: function() {
this.columns = this.options.columns;
this.options = _.defaults(this.options, {
paginated: false,
page: 1,
perPage: 10,
tableClassName: 'table'
});
this.collection.on('reset', this.render, this);
this._prepare();
},
render: function() {
this.$el.empty();
this.renderTable();
if (this.options.paginated) {
this.renderPagination();
}
return this;
},
renderTable: function() {
var $table = $('<table></table>', {'class': this.options.tableClassName});
this.$el.append($table);
var header = new Header({columns: this.columns, sorter: this.sorter});
$table.append(header.render().el);
$table.append('<tbody></tbody>');
this.collection.forEach(this.renderRow, this);
},
renderPagination: function() {
var pagination = new Pagination({pager: this.pager});
this.$el.append(pagination.render().el);
},
renderRow: function(model) {
var options = {
model: model,
columns: this.columns
};
var rowClassName = this.options.rowClassName;
if (_.isFunction(rowClassName)) {
rowClassName = rowClassName(model);
}
options.className = rowClassName;
var row = new Row(options);
this.$('tbody').append(row.render(this.columns).el);
},
refresh: function(options) {
if (this.options.paginated) {
this._page(options);
} else {
if (this.options.inMemory) {
this.collection.trigger('reset', this.collection);
if (options && options.success) {
options.success();
}
} else {
this._request(options);
}
}
},
sort: function(column, order) {
this.sorter.sort(column, order);
},
page: function(page) {
this.pager.page(page);
},
perPage: function(perPage) {
this.pager.set('perPage', perPage);
},
_sort: function() {
if (this.options.inMemory) {
this._sortInMemory();
} else {
this._sortRequest();
}
},
_sortInMemory: function() {
if (this.options.paginated) {
this._originalCollection.comparator = _.bind(this._comparator, this);
this._originalCollection.sort();
this.page(1);
} else {
this.collection.comparator = _.bind(this._comparator, this);
this.collection.sort();
}
},
_comparator: function(model1, model2) {
var columnComparator = this._comparatorForColumn(this.sorter.get('column'));
var order = columnComparator(model1, model2);
return this.sorter.sortedASC() ? order : -order;
},
_comparatorForColumn: function(column) {
var c = _.find(this.columns, function(c) {
return c.property === column || c.index === column;
});
return c ? c.comparator : undefined;
},
_sortRequest: function() {
this._request();
},
_page: function(options) {
if (this.options.inMemory) {
this._pageInMemory(options);
} else {
this._pageRequest(options);
}
},
_pageRequest: function(options) {
this._request(options);
},
_request: function(options) {
options = options || {};
var success = options.success;
var silent = options.silent;
options.data = this._getRequestData();
options.success = _.bind(function(collection) {
if (!this.columns || _.isEmpty(this.columns)) {
this._prepareColumns();
}
if (success) {
success();
}
if (this.options.paginated) {
this.pager.update(collection);
}
if (!silent) {
collection.trigger('reset', collection);
}
}, this);
options.silent = true;
this.collection.fetch(options);
},
_getRequestData: function() {
if (this.collection.data && _.isFunction(this.collection.data)) {
return this.collection.data(this.pager, this.sorter);
} else if (this.collection.data && typeof this.collection.data === 'object') {
var data = {};
_.each(this.collection.data, function(value, param) {
if (_.isFunction(value)) {
value = value(this.pager, this.sorter);
}
data[param] = value;
}, this);
return data;
} else if (this.options.paginated) {
return {
page: this.pager.get('currentPage'),
per_page: this.pager.get('perPage')
};
}
return {};
},
_pageInMemory: function(options) {
if (!this._originalCollection) {
this._originalCollection = this.collection.clone();
}
var page = this.pager.get('currentPage');
var perPage = this.pager.get('perPage');
var begin = (page - 1) * perPage;
var end = begin + perPage;
if (options && options.success) {
options.success();
}
this.pager.set('total', this._originalCollection.size());
this.collection.reset(this._originalCollection.slice(begin, end), options);
},
_prepare: function() {
this._prepareSorter();
this._preparePager();
this._prepareColumns();
this.refresh();
},
_prepareSorter: function() {
this.sorter = new Sorter();
this.sorter.on('change', function() {
this._sort(this.sorter.get('column'), this.sorter.get('order'));
}, this);
},
_preparePager: function() {
this.pager = new Pager({
currentPage: this.options.page,
perPage: this.options.perPage
});
this.pager.on('change:currentPage', function () {
this._page();
}, this);
this.pager.on('change:perPage', function() {
this.page(1);
}, this);
},
_prepareColumns: function() {
if (!this.columns || _.isEmpty(this.columns)) {
this._defaultColumns();
} else {
_.each(this.columns, function(column, i) {
this.columns[i] = this._prepareColumn(column, i);
}, this);
}
},
_prepareColumn: function(column, index) {
if (_.isString(column)) {
column = { property: column };
}
if (_.isObject(column)) {
column.index = index;
if (column.property) {
column.title = column.title || this._formatTitle(column.property);
} else if (!column.property && !column.view) {
throw new Error('Column \'' + column.title + '\' has no property and must accordingly define a custom cell view.');
}
if (this.options.inMemory && column.sortable) {
if (!column.comparator && !column.property && !column.sortedProperty) {
throw new Error('Invalid column definition: a sortable column must have a comparator, property or sortedProperty defined.');
}
column.comparator = column.comparator || this._defaultComparator(column.sortedProperty || column.property);
}
}
return column;
},
_formatTitle: function(title) {
return _.map(title.split(/\s|_/), function(word) {
return word.charAt(0).toUpperCase() + word.substr(1);
}).join(' ');
},
_defaultColumns: function() {
this.columns = [];
var model = this.collection.first(), i = 0;
if (model) {
for (var p in model.toJSON()) {
this.columns.push(this._prepareColumn(p, i++));
}
}
},
_defaultComparator: function(column) {
return function(model1, model2) {
var val1 = model1.has(column) ? model1.get(column) : '';
var val2 = model2.has(column) ? model2.get(column) : '';
return val1.localeCompare(val2);
};
}
});
var Header = Datagrid.Header = Backbone.View.extend({
tagName: 'thead',
initialize: function() {
this.columns = this.options.columns;
this.sorter = this.options.sorter;
},
render: function() {
var model = new Backbone.Model();
var headerColumn, columns = [];
_.each(this.columns, function(column, i) {
headerColumn = _.clone(column);
headerColumn.property = column.property || column.index;
headerColumn.view = column.headerView || {
type: HeaderCell,
sorter: this.sorter
};
model.set(headerColumn.property, column.title);
columns.push(headerColumn);
}, this);
var row = new Row({model: model, columns: columns, header: true});
this.$el.html(row.render().el);
return this;
}
});
var Row = Datagrid.Row = Backbone.View.extend({
tagName: 'tr',
initialize: function() {
this.columns = this.options.columns;
this.model.on('change', this.render, this);
},
render: function() {
this.$el.empty();
_.each(this.columns, this.renderCell, this);
return this;
},
renderCell: function(column) {
var cellView = this._resolveCellView(column);
this.$el.append(cellView.render().el);
},
_resolveCellView: function(column) {
var options = {
model: this.model,
column: column
};
if (this.options.header || column.header) {
options.tagName = 'th';
}
var cellClassName = column.cellClassName;
if (_.isFunction(cellClassName)) {
cellClassName = cellClassName(this.model);
}
options.className = cellClassName;
var view = column.view || Cell;
// Resolve view from string or function
if (typeof view !== 'object' && !(view.prototype && view.prototype.render)) {
if (_.isString(view)) {
options.callback = _.template(view);
view = CallbackCell;
} else if (_.isFunction(view) && !view.prototype.render) {
options.callback = view;
view = CallbackCell;
} else {
throw new TypeError('Invalid view passed to column "' + column.title + '".');
}
}
// Resolve view from options
else if (typeof view === 'object') {
_.extend(options, view);
view = view.type;
if (!view || !view.prototype || !view.prototype.render) {
throw new TypeError('Invalid view passed to column "' + column.title + '".');
}
}
return new view(options);
}
});
var Pagination = Datagrid.Pagination = Backbone.View.extend({
className: 'pagination pagination-centered',
events: {
'click li:not(.disabled) a': 'page',
'click li.disabled a': function(e) { e.preventDefault(); }
},
initialize: function() {
this.pager = this.options.pager;
},
render: function() {
var $ul = $('<ul></ul>'), $li;
$li = $('<li class="prev"><a href="#">«</a></li>');
if (!this.pager.hasPrev()) {
$li.addClass('disabled');
}
$ul.append($li);
if (this.pager.hasTotal()) {
for (var i = 1; i <= this.pager.get('totalPages'); i++) {
$li = $('<li></li>');
if (i === this.pager.get('currentPage')) {
$li.addClass('active');
}
$li.append('<a href="#">' + i + '</a>');
$ul.append($li);
}
}
$li = $('<li class="next"><a href="#">»</a></li>');
if (!this.pager.hasNext()) {
$li.addClass('disabled');
}
$ul.append($li);
this.$el.append($ul);
return this;
},
page: function(event) {
var $target = $(event.target), page;
if ($target.parent().hasClass('prev')) {
this.pager.prev();
} else if ($target.parent().hasClass('next')) {
this.pager.next();
}
else {
this.pager.page(parseInt($(event.target).html(), 10));
}
return false;
}
});
var Cell = Datagrid.Cell = Backbone.View.extend({
tagName: 'td',
initialize: function() {
this.column = this.options.column;
},
render: function() {
this._prepareValue();
this.$el.html(this.value);
return this;
},
_prepareValue: function() {
this.value = this.model.get(this.column.property);
}
});
var CallbackCell = Datagrid.CallbackCell = Cell.extend({
initialize: function() {
CallbackCell.__super__.initialize.call(this);
this.callback = this.options.callback;
},
_prepareValue: function() {
this.value = this.callback(this.model.toJSON());
}
});
var ActionCell = Datagrid.ActionCell = Cell.extend({
initialize: function() {
ActionCell.__super__.initialize.call(this);
},
action: function() {
return this.options.action(this.model);
},
_prepareValue: function() {
var a = $('<a></a>');
a.html(this.options.label);
a.attr('href', this.options.href || '#');
if (this.options.actionClassName) {
a.addClass(this.options.actionClassName);
}
if (this.options.action) {
this.delegateEvents({
'click a': this.action
});
}
this.value = a;
}
});
var HeaderCell = Datagrid.HeaderCell = Cell.extend({
initialize: function() {
HeaderCell.__super__.initialize.call(this);
this.sorter = this.options.sorter;
if (this.column.sortable) {
this.delegateEvents({click: 'sort'});
}
},
render: function() {
this._prepareValue();
var html = this.value, icon;
if (this.column.sortable) {
this.$el.addClass('sortable');
if (this.sorter.sortedBy(this.column.sortedProperty || this.column.property) || this.sorter.sortedBy(this.column.index)) {
if (this.sorter.sortedASC()) {
icon = 'icon-chevron-up';
} else {
icon = 'icon-chevron-down';
}
} else {
icon = 'icon-minus';
}
html += ' <i class="' + icon + ' pull-right"></i>';
}
this.$el.html(html);
return this;
},
sort: function() {
this.sorter.sort(this.column.sortedProperty || this.column.property);
}
});
var Pager = Datagrid.Pager = Backbone.Model.extend({
initialize: function() {
this.on('change:perPage change:total', function() {
this.totalPages(this.get('total'));
}, this);
if (this.has('total')) {
this.totalPages(this.get('total'));
}
},
update: function(options) {
_.each(['hasNext', 'hasPrev', 'total', 'totalPages', 'lastPage'], function(p) {
if (!_.isUndefined(options[p])) {
this.set(p, options[p]);
}
}, this);
},
totalPages: function(total) {
if (_.isNumber(total)) {
this.set('totalPages', Math.ceil(total/this.get('perPage')));
} else {
this.set('totalPages', undefined);
}
},
page: function(page) {
if (this.inBounds(page)) {
if (page === this.get('currentPage')) {
this.trigger('change:currentPage');
} else {
this.set('currentPage', page);
}
}
},
next: function() {
this.page(this.get('currentPage') + 1);
},
prev: function() {
this.page(this.get('currentPage') - 1);
},
hasTotal: function() {
return this.has('totalPages');
},
hasNext: function() {
if (this.hasTotal()) {
return this.get('currentPage') < this.get('totalPages');
} else {
return this.get('hasNext');
}
},
hasPrev: function() {
if (this.has('hasPrev')) {
return this.get('hasPrev');
} else {
return this.get('currentPage') > 1;
}
},
inBounds: function(page) {
return !this.hasTotal() || page > 0 && page <= this.get('totalPages');
},
validate: function(attrs) {
if (attrs.perPage < 1) {
throw new Error('perPage must be greater than zero.');
}
}
});
var Sorter = Datagrid.Sorter = Backbone.Model.extend({
sort: function(column, order) {
if (!order && this.get('column') === column) {
this.toggleOrder();
} else {
this.set({
column: column,
order: order || Sorter.ASC
});
}
},
sortedBy: function(column) {
return this.get('column') === column;
},
sortedASC: function() {
return this.get('order') === Sorter.ASC;
},
sortedDESC: function() {
return this.get('order') === Sorter.DESC;
},
toggleOrder: function() {
if (this.get('order') === Sorter.ASC) {
this.set('order', Sorter.DESC);
} else {
this.set('order', Sorter.ASC);
}
}
});
Sorter.ASC = 'asc';
Sorter.DESC = 'desc';
Backbone.Datagrid = Datagrid;
})();
// backbone.datagrid v0.3.2-alpha
//
// Copyright (c) 2012 Loïc Frering <[email protected]>
// Distributed under the MIT license
(function(){var e=Backbone.View.extend({initialize:function(){this.columns=this.options.columns,this.options=_.defaults(this.options,{paginated:!1,page:1,perPage:10,tableClassName:"table"}),this.collection.on("reset",this.render,this),this._prepare()},render:function(){return this.$el.empty(),this.renderTable(),this.options.paginated&&this.renderPagination(),this},renderTable:function(){var e=$("<table></table>",{"class":this.options.tableClassName});this.$el.append(e);var n=new t({columns:this.columns,sorter:this.sorter});e.append(n.render().el),e.append("<tbody></tbody>"),this.collection.forEach(this.renderRow,this)},renderPagination:function(){var e=new r({pager:this.pager});this.$el.append(e.render().el)},renderRow:function(e){var t={model:e,columns:this.columns},r=this.options.rowClassName;_.isFunction(r)&&(r=r(e)),t.className=r;var i=new n(t);this.$("tbody").append(i.render(this.columns).el)},refresh:function(e){this.options.paginated?this._page(e):this.options.inMemory?(this.collection.trigger("reset",this.collection),e&&e.success&&e.success()):this._request(e)},sort:function(e,t){this.sorter.sort(e,t)},page:function(e){this.pager.page(e)},perPage:function(e){this.pager.set("perPage",e)},_sort:function(){this.options.inMemory?this._sortInMemory():this._sortRequest()},_sortInMemory:function(){this.options.paginated?(this._originalCollection.comparator=_.bind(this._comparator,this),this._originalCollection.sort(),this.page(1)):(this.collection.comparator=_.bind(this._comparator,this),this.collection.sort())},_comparator:function(e,t){var n=this._comparatorForColumn(this.sorter.get("column")),r=n(e,t);return this.sorter.sortedASC()?r:-r},_comparatorForColumn:function(e){var t=_.find(this.columns,function(t){return t.property===e||t.index===e});return t?t.comparator:undefined},_sortRequest:function(){this._request()},_page:function(e){this.options.inMemory?this._pageInMemory(e):this._pageRequest(e)},_pageRequest:function(e){this._request(e)},_request:function(e){e=e||{};var t=e.success,n=e.silent;e.data=this._getRequestData(),e.success=_.bind(function(e){(!this.columns||_.isEmpty(this.columns))&&this._prepareColumns(),t&&t(),this.options.paginated&&this.pager.update(e),n||e.trigger("reset",e)},this),e.silent=!0,this.collection.fetch(e)},_getRequestData:function(){if(this.collection.data&&_.isFunction(this.collection.data))return this.collection.data(this.pager,this.sorter);if(this.collection.data&&typeof this.collection.data=="object"){var e={};return _.each(this.collection.data,function(t,n){_.isFunction(t)&&(t=t(this.pager,this.sorter)),e[n]=t},this),e}return this.options.paginated?{page:this.pager.get("currentPage"),per_page:this.pager.get("perPage")}:{}},_pageInMemory:function(e){this._originalCollection||(this._originalCollection=this.collection.clone());var t=this.pager.get("currentPage"),n=this.pager.get("perPage"),r=(t-1)*n,i=r+n;e&&e.success&&e.success(),this.pager.set("total",this._originalCollection.size()),this.collection.reset(this._originalCollection.slice(r,i),e)},_prepare:function(){this._prepareSorter(),this._preparePager(),this._prepareColumns(),this.refresh()},_prepareSorter:function(){this.sorter=new f,this.sorter.on("change",function(){this._sort(this.sorter.get("column"),this.sorter.get("order"))},this)},_preparePager:function(){this.pager=new a({currentPage:this.options.page,perPage:this.options.perPage}),this.pager.on("change:currentPage",function(){this._page()},this),this.pager.on("change:perPage",function(){this.page(1)},this)},_prepareColumns:function(){!this.columns||_.isEmpty(this.columns)?this._defaultColumns():_.each(this.columns,function(e,t){this.columns[t]=this._prepareColumn(e,t)},this)},_prepareColumn:function(e,t){_.isString(e)&&(e={property:e});if(_.isObject(e)){e.index=t;if(e.property)e.title=e.title||this._formatTitle(e.property);else if(!e.property&&!e.view)throw new Error("Column '"+e.title+"' has no property and must accordingly define a custom cell view.");if(this.options.inMemory&&e.sortable){if(!e.comparator&&!e.property&&!e.sortedProperty)throw new Error("Invalid column definition: a sortable column must have a comparator, property or sortedProperty defined.");e.comparator=e.comparator||this._defaultComparator(e.sortedProperty||e.property)}}return e},_formatTitle:function(e){return _.map(e.split(/\s|_/),function(e){return e.charAt(0).toUpperCase()+e.substr(1)}).join(" ")},_defaultColumns:function(){this.columns=[];var e=this.collection.first(),t=0;if(e)for(var n in e.toJSON())this.columns.push(this._prepareColumn(n,t++))},_defaultComparator:function(e){return function(t,n){var r=t.has(e)?t.get(e):"",i=n.has(e)?n.get(e):"";return r.localeCompare(i)}}}),t=e.Header=Backbone.View.extend({tagName:"thead",initialize:function(){this.columns=this.options.columns,this.sorter=this.options.sorter},render:function(){var e=new Backbone.Model,t,r=[];_.each(this.columns,function(n,i){t=_.clone(n),t.property=n.property||n.index,t.view=n.headerView||{type:u,sorter:this.sorter},e.set(t.property,n.title),r.push(t)},this);var i=new n({model:e,columns:r,header:!0});return this.$el.html(i.render().el),this}}),n=e.Row=Backbone.View.extend({tagName:"tr",initialize:function(){this.columns=this.options.columns,this.model.on("change",this.render,this)},render:function(){return this.$el.empty(),_.each(this.columns,this.renderCell,this),this},renderCell:function(e){var t=this._resolveCellView(e);this.$el.append(t.render().el)},_resolveCellView:function(e){var t={model:this.model,column:e};if(this.options.header||e.header)t.tagName="th";var n=e.cellClassName;_.isFunction(n)&&(n=n(this.model)),t.className=n;var r=e.view||i;if(typeof r!="object"&&(!r.prototype||!r.prototype.render))if(_.isString(r))t.callback=_.template(r),r=s;else{if(!_.isFunction(r)||!!r.prototype.render)throw new TypeError('Invalid view passed to column "'+e.title+'".');t.callback=r,r=s}else if(typeof r=="object"){_.extend(t,r),r=r.type;if(!r||!r.prototype||!r.prototype.render)throw new TypeError('Invalid view passed to column "'+e.title+'".')}return new r(t)}}),r=e.Pagination=Backbone.View.extend({className:"pagination pagination-centered",events:{"click li:not(.disabled) a":"page","click li.disabled a":function(e){e.preventDefault()}},initialize:function(){this.pager=this.options.pager},render:function(){var e=$("<ul></ul>"),t;t=$('<li class="prev"><a href="#">«</a></li>'),this.pager.hasPrev()||t.addClass("disabled"),e.append(t);if(this.pager.hasTotal())for(var n=1;n<=this.pager.get("totalPages");n++)t=$("<li></li>"),n===this.pager.get("currentPage")&&t.addClass("active"),t.append('<a href="#">'+n+"</a>"),e.append(t);return t=$('<li class="next"><a href="#">»</a></li>'),this.pager.hasNext()||t.addClass("disabled"),e.append(t),this.$el.append(e),this},page:function(e){var t=$(e.target),n;return t.parent().hasClass("prev")?this.pager.prev():t.parent().hasClass("next")?this.pager.next():this.pager.page(parseInt($(e.target).html(),10)),!1}}),i=e.Cell=Backbone.View.extend({tagName:"td",initialize:function(){this.column=this.options.column},render:function(){return this._prepareValue(),this.$el.html(this.value),this},_prepareValue:function(){this.value=this.model.get(this.column.property)}}),s=e.CallbackCell=i.extend({initialize:function(){s.__super__.initialize.call(this),this.callback=this.options.callback},_prepareValue:function(){this.value=this.callback(this.model.toJSON())}}),o=e.ActionCell=i.extend({initialize:function(){o.__super__.initialize.call(this)},action:function(){return this.options.action(this.model)},_prepareValue:function(){var e=$("<a></a>");e.html(this.options.label),e.attr("href",this.options.href||"#"),this.options.actionClassName&&e.addClass(this.options.actionClassName),this.options.action&&this.delegateEvents({"click a":this.action}),this.value=e}}),u=e.HeaderCell=i.extend({initialize:function(){u.__super__.initialize.call(this),this.sorter=this.options.sorter,this.column.sortable&&this.delegateEvents({click:"sort"})},render:function(){this._prepareValue();var e=this.value,t;return this.column.sortable&&(this.$el.addClass("sortable"),this.sorter.sortedBy(this.column.sortedProperty||this.column.property)||this.sorter.sortedBy(this.column.index)?this.sorter.sortedASC()?t="icon-chevron-up":t="icon-chevron-down":t="icon-minus",e+=' <i class="'+t+' pull-right"></i>'),this.$el.html(e),this},sort:function(){this.sorter.sort(this.column.sortedProperty||this.column.property)}}),a=e.Pager=Backbone.Model.extend({initialize:function(){this.on("change:perPage change:total",function(){this.totalPages(this.get("total"))},this),this.has("total")&&this.totalPages(this.get("total"))},update:function(e){_.each(["hasNext","hasPrev","total","totalPages","lastPage"],function(t){_.isUndefined(e[t])||this.set(t,e[t])},this)},totalPages:function(e){_.isNumber(e)?this.set("totalPages",Math.ceil(e/this.get("perPage"))):this.set("totalPages",undefined)},page:function(e){this.inBounds(e)&&(e===this.get("currentPage")?this.trigger("change:currentPage"):this.set("currentPage",e))},next:function(){this.page(this.get("currentPage")+1)},prev:function(){this.page(this.get("currentPage")-1)},hasTotal:function(){return this.has("totalPages")},hasNext:function(){return this.hasTotal()?this.get("currentPage")<this.get("totalPages"):this.get("hasNext")},hasPrev:function(){return this.has("hasPrev")?this.get("hasPrev"):this.get("currentPage")>1},inBounds:function(e){return!this.hasTotal()||e>0&&e<=this.get("totalPages")},validate:function(e){if(e.perPage<1)throw new Error("perPage must be greater than zero.")}}),f=e.Sorter=Backbone.Model.extend({sort:function(e,t){!t&&this.get("column")===e?this.toggleOrder():this.set({column:e,order:t||f.ASC})},sortedBy:function(e){return this.get("column")===e},sortedASC:function(){return this.get("order")===f.ASC},sortedDESC:function(){return this.get("order")===f.DESC},toggleOrder:function(){this.get("order")===f.ASC?this.set("order",f.DESC):this.set("order",f.ASC)}});f.ASC="asc",f.DESC="desc",Backbone.Datagrid=e})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment