var user = []; var timelines = []; var activeTimeline = []; var entries = []; var newEntryBar; var moreEntriesbar; var currentEntryOffset = 0; var loadLength = 20; /* Setup after the document is ready for manipulation. */ $(document).ready(function(){ // looked up once and remembered newEntryBar = $("#new-entry"); moreEntriesBar = $("#more-entries"); // wire the login dialog using jQuery UI $("#login-dialog").dialog({ autoOpen: false, height: 300, width: 300, modal: true, buttons: { Login: function(){login()} } }); // TODO: add a hook to AJAX requests to check for 401 unauth // and re-display the login dialog. // try to load user information for an authenticated user $.ajax({ url: "/ts_api/users/", type: "GET", error: function(jqXHR, textStatus, error) { // assume there is no authenticated user, show login dialog $("#login-dialog").dialog("open"); }, success: function(data, textStatus, jqXHR) { // load the user information loadUser(data.user.username); }}); }) /* Read the user's credentials from the login form and perform * an AJAX request to the API to set the session cookie. */ function login() { // lookup the login dialog elements var name = $("#login-name"); var pwd = $("#login-password"); // call the API via AJAX $.ajax({ url: "/ts_api/login", processData: false, data: JSON.stringify({username: name.val(), password: pwd.val()}), type: "POST", error: function(jqXHR, textStatus, error) { // assuming bad credentials (possible server error or bad request, // we should check that, FIXME var tips = $(".validate-tips"); tips.text("Incorrect username/password combination."); tips.addClass("ui-state-error"); tips.slideDown(); }, success: function(data, textStatus, jqXHR) { // load the user information and hide the login dialog loadUser(name.val()); $("#login-dialog").dialog("close"); }}); } /* End the current user session and expire any session credentials we * have aquired. */ function logout(event) { alert("TODO: log user out via AJAX."); // TODO: wipe username, timeline, entry variables and displays event.preventDefault(); } /* Load and display the user's information and timelines. */ function loadUser(username) { // call the user_summary API function $.ajax({ url: "/ts_api/app/user_summary/" + username, type: "GET", success: function(data, textStatus, jqXHR) { // set the user variable user = data.user; // set the timelines variable timelines = data.timelines; // update the user id display $("#fullname").text(user.name); $("#username").text("- " + user.username); // pre-populate the editable user-info fields $("#fullname-input").val(user.name); $("#email-input").val(user.email); // set the active timeline to the first in the list // TODO: implement a mechanism to remember the last used timeline // on the server side and respond to that here. activeTimeline = timelines[0]; // update the timeline display $("#timeline-name").text(activeTimeline.timeline_id + " |"); $("#timeline-desc").text(activeTimeline.description); $("#timeline-desc-input").val(activeTimeline.description); // TODO: populate the drop-down list for the available timeline // choices // load the entries for this timeline loadEntries(user, activeTimeline, "new") }, error: function(jqXHR, textStatus, error) { // TODO alert("TODO: handle error for user load.") alert(jqXHR.responseText) } }); } /* Read entries for a timeline. */ function loadEntries(user, timeline, order) { // call the API list_entries function via AJAX $.ajax({ url: "/ts_api/entries/" + user.username + "/" + timeline.timeline_id + "?order=asc&start=" + currentEntryOffset + "&length=" + loadLength, type: "GET", success: function(data, textStatus, jqXHR) { entries = data.entries; if (order == "new") { // reverse the list so that the oldest are first entries.reverse() // push the entries onto the page displayNewerEntries(entries) } else { displayOlderEntries(entries) } // update our current offset currentEntryOffset += loadLength; }, error: function(jqXHR, textStatus, error) { alert(jqXHR.responseText); } }); } /* Push the entries onto the top of the entry display. This function assumes * that the entries are sorted by id, ascending (0, 1, 2). Each new entry goes * on top, pushing down all existing entries. So the first entry in the * argument list ends up at the bottom of the new entries. */ function displayNewerEntries(entries) { // for each entry _.each(entries, function(entry) { // remove the existing top-entry designation $(".top-entry").removeClass("top-entry"); // create the new entry from the entry template (see ICanHas.js) var entryElem = ich.entry(entry); // mark the new entry as the current top-entry entryElem.addClass("top-entry"); // push the entry on after the new-entry bar (on top of existing // entries). newEntryBar.after(entryElem); // TODO: animate? If so, may need to pause after each to allow // some animation to take place. Going for a continuous push-down // stack effect. }); } /* Push the entries onto the bottom of the entry display. The function assumes * that the entries are sorted by id, descending (2, 1, 0). Each new entry goes * at the bottom of the stack of entries, extending the stack downwards. So the * last entry in the list is at the very bottom of the stack. */ function displayOlderEntries(entries) { // for each entry _.each(entries, function(entry) { // create the new entry from the template (ICanHas.js) var entryElem = ich.entry(entry); // place the entry on top of the bottom of the page moreEntriesBar.before(entryElem); // perhaps animate, see comments at the end of displayNewerEntries() }); } /* Update the user information based on the editable user-info panel. */ function updateUser(event) { alert("TODO: update user via AJAX."); event.preventDefault(); } /* Show the change timeline menu. */ function showTimelineMenu(event) { alert("TODO: show other timelines via a popup menu"); event.preventDefault(); } /* Update the timeline details based on the editable timeline-info panel. */ function updateTimeline(event) { var desc = $("#timeline-desc-input").val(); $.ajax({url: "/ts_api/timelines/" + user.username + "/" + activeTimeline.timeline_id, type: "POST", data: JSON.stringify({desc: desc, created: activeTimeline.created}), error: function(jqXHR, textStatus, error) { // TODO: better error handling alert("Error updating timeline: \n" + jqXHR.responseText); }, success: function(data, testStatus, jqXHR) { // TODO: check for appropriate data.status value // update display $("#timeline-desc").text(data.timeline.description); }}); event.preventDefault(); } /* Create a new entry based on the user's input in the new-entry panel. */ function newEntry(event) { var mark = $("#new-entry-input").val(); var notes = $("#new-notes-input").val(); var timestamp = getUTCTimestamp(); // timestamp is handled on client. // This is important. It means that the timestamps are completely in the // hands of the user. Timestamps from this service can not be considered // secure. This service is not a punch-card, but a time-tracker, voluntary // and editable by the user. var payload = JSON.stringify( { mark: mark, notes: notes, timestamp: timestamp }); $.ajax({url: "/ts_api/entries/" + user.username + "/" + activeTimeline.timeline_id, type: "PUT", data: payload, error: function(jqXHR, textStatus, error) { // TODO: better error handling. alert("Error creating entry: \n" + jqXHR.responseText); }, success: function(data, textStatus, jqXHR) { // TODO: check that data.status == "ok" // grab the entry data var newEntry = data.entry; // push the entry on top of the stack displayNewerEntries([newEntry]); // clear the input $("#new-entry-input").val(""); $("#new-notes-input").val(""); }}); event.preventDefault(); } function toggleEditEntry(event, entryId) { $("#entry-" + entryId + " .entry-display").toggle(); $("#entry-" + entryId + " .entry-edit").toggle(); event.preventDefault(); } function updateEntry(event, entryId) { var mark = $("#entry-" + entryId + "-mark-input").val(); var notes = $("#entry-" + entryId + "-notes-input").val(); var timestamp = getUTCTimestamp(); // TODO: define and read from input element var payload = JSON.stringify( { mark: mark, notes: notes, timestamp: timestamp }); $.ajax({url: "/ts_api/entries/" + user.username + "/" + activeTimeline.timeline_id + "/" + entryId, type: "POST", data: payload, error: function(jqXHR, textStatus, error) { // TODO: error handling alert("Error updating entry: \n" + jqXHR.responseText); }, success: function(data, textStatus, jqXHR) { // TODO: check that data.status is appropriate // update the entry display $("#entry-" + entryId + " .entry-mark").text(data.entry.mark); $("#entry-" + entryId + " .entry-notes").text(data.entry.notes); }}); toggleEditEntry(event, entryId); } /* Delete an entry. */ function deleteEntry(event, entryId) { $.ajax({url: "/ts_api/entries/" + user.username + "/" + activeTimeline.timeline_id + "/" + entryId, type: "DELETE", error: function(jqXHR, textStatus, error) { // TODO: error handling alert("Error updating entry: \n" + jqXHR.responseText); }, success: function(data, textStatus, jqXHR) { $("#entry-" + entryId).slideUp('slow', function() {$("#entry-" + entryId).remove(); }); }}); event.preventDefault(); } /* Generate a UTC timestamp string in ISO format. * Javascript 1.8.5 has Date.toJSON(), which is equivalent to this function, * but is only supported in Firefox 4. */ function getUTCTimestamp() { var d = new Date(); function pad(n){return n<10 ? '0'+n : n} return d.getUTCFullYear()+'-' + pad(d.getUTCMonth()+1)+'-' + pad(d.getUTCDate())+'T' + pad(d.getUTCHours())+':' + pad(d.getUTCMinutes())+':' + pad(d.getUTCSeconds())+'Z'; }