Merge branch 'client-redesign'

Conflicts:
	yaws.dev.conf
This commit is contained in:
Jonathan Bernard 2011-06-07 08:38:09 -05:00
commit bc364b2ebd
74 changed files with 5126 additions and 2511 deletions

View File

@ -3,22 +3,23 @@ BEAMS = $(MODS:src/%.erl=ebin/%.beam)
TEST_MODS = $(wildcard test/*.erl)
TEST_BEAMS = $(TEST_MODS:test/%.erl=test/%.beam)
TS_ROOT=/usr/lib/yaws/jdb-labs/timestamper
CWD = `pwd`
default: compile
all : compile test
compile : $(BEAMS)
compile : init $(BEAMS)
compile-test : $(TEST_BEAMS)
compile-test : init $(TEST_BEAMS)
test : start-test-server run-test stop-test-server
test-shell : compile compile-test
test-shell : compile compile-test config-yaws-dev
@echo Starting an interactive YAWS shell with test paths loaded.
@yaws -i --pa test --id test_inst
run-test : compile compile-test
run-test : compile compile-test config-yaws-dev
@erl -pa ./ebin -pa ./test -run timestamper_api_tests test -run init stop -noshell
start-test-server :

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
db/test/ts_entry.DCL Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

4
doc/features.todo.txt Normal file
View File

@ -0,0 +1,4 @@
- Switch to local storage if unable to reach the server, sync when server is
available.
- Provide full-text search on timestamp marks and notes. Use Lucene in a
seperate process? Build our own Erlang indexing code?

4
doc/issues/0000tn4.rst Normal file
View File

@ -0,0 +1,4 @@
Refactor models and views.
==========================
Try to find the behavior that is common to mobile and desktop versions.

View File

@ -0,0 +1,2 @@
Add UI for note taking.
=======================

View File

@ -0,0 +1,9 @@
Add Markdown converter for notes.
=================================
Brief description.
========= ==========
Created: 2011-05-15
Resolved: 2011-05-15
========= ==========

View File

@ -0,0 +1,11 @@
Duration mis-set on new entries.
================================
Fix the duration bug when adding new events. Need to set the nextModel for
the previously 'current' timestamp and set the nextModel of the new timestamp
to 'null'
========= ==========
Created: 2011-05-15
Resolved: 2011-05-15
========= ==========

View File

@ -0,0 +1,10 @@
Generate day seperators.
========================
When generating EventViews in the EventListView, we need to automatically
create and insert day seperators (see prototype).
========= ==========
Created: 2011-05-15
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,10 @@
Fix UI for tasks with a duration a day or longer.
=================================================
Tasks that are extremely long-running can overflow the space set aside for
the *Duration* column.
========= ==========
Created: 2011-05-15
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,14 @@
Fix user menu UI.
=================
UI for user menu does not work.
Resolution
----------
UI menu changed to be displayed to the right of the username, in the empty black space vailable.
========= ==========
Created: 2011-05-15
Resolved: 2011-06-01
========= ==========

View File

@ -0,0 +1,9 @@
Fix timeline menu UI.
=====================
UI for timeline menu does not work.
========= ==========
Created: 2011-05-15
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,9 @@
Implement timeline selection.
=============================
Allow the user to change timelines.
========= ==========
Created: 2011-05-15
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,15 @@
Create UI for timeline creation.
================================
There should be a way through the interface for a user to create a new
timeline.
Resolution
----------
Abstracted the login dialog CSS to support other dialogs of the same look and feel. Used this to create a dialog for creating new timelines.
========= ==========
Created: 2011-05-15
Resolved: 2011-06-01
========= ==========

View File

@ -0,0 +1,9 @@
Implement a date-picker for start time.
=======================================
Use a custom jQuery UI theme?
========= ==========
Created: 2011-05-15
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,18 @@
Implement UI for entry re-order when chronological order changes.
=================================================================
The UI should re-order the EntryList display when the chronological of the entries
changes based on user input.
Two ways to do this spring to mind:
1. ``slideUp`` the EntryView at it's original position, find the new position,
move it in the DOM and ``slideDown`` the element into view.
2. Detach the element from the list, position it absolutely, animate it to it's new
absolute position (based on the position of its new neighbors) and re-insert it
into the list.
========= ==========
Created: 2011-05-15
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,10 @@
Create a subtle alternating background for EntryViews
=====================================================
The goal is to visually tie together elements in the same row, and subtley
distinquish each row from its neighbors.
========= ==========
Created: 2011-05-15
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,9 @@
Create tooltip/some help system.
================================
Need some way to make actions discoverable and easy to understand.
========= ==========
Created: 2011-05-15
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,10 @@
Create a real-time tick-tock for the current entry duration.
============================================================
Have some visible indication that the display is being updated as time passes.
Blink the field, or just the units.
========= ==========
Created: 2011-05-15
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,10 @@
Automatic code highlighting.
============================
Look at content in ``<pre>`` and ``<code>`` blocks and see if we can highlight it.
Planning to use Highlight.js to do this.
========= ==========
Created: 2011-05-15
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,9 @@
Create new timeline button in timeline menu.
============================================
Need a button to trigger the new timeline dialog.
========= ==========
Created: 2011-06-01
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,7 @@
Duration of next previous entries are not updated when a timestamp is updated.
==============================================================================
========= ==========
Created: 2011-06-01
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1,7 @@
Implement timeline creation.
============================
========= ==========
Created: 2011-05-15
Resolved: YYYY-MM-DD
========= ==========

View File

@ -0,0 +1 @@
Prototype out mobile workflow.

34
doc/model.txt Normal file
View File

@ -0,0 +1,34 @@
Data
----
UserModel
TimelineModel
TimelineListModel
EntryListModel
EntryModel
Views
-----
EntryView
NewEntryInput
TimelineListView
TimelineView
UserView
Data Dependencies
-----------------
UserModel: none
TimelineModel: none
TimelineListModel: UserModel
EntryModel: none
EntryListModel: TimelineModel
View Dependencies
-----------------
UserView: UserModel
TimelineView: TimelineModel, UserView
TimelineListView: TimelineListModel, UserView
EntryView: EntryModel, EntryListView
EntryListView: EntryListModel, TimelineView

BIN
doc/model.xcf Normal file

Binary file not shown.

29
doc/todo.rst Normal file
View File

@ -0,0 +1,29 @@
Current
=======
Upcoming
========
- Generate day seperators
- Fix UI for tasks with a duration a day or longer
- Fix hover UI for user menu
- Fix hover UI for timeline menu
- Test (implement?) timeline selection
- Add UI for timeline creation
- Change the UI for editing the start-time. Use a date-picker (custom jQuery
UI theme?)
- Fix UI for timestamp edits which change the order of events in the timeline.
- Create a light, alternating background for entries
- Add hover-enabled icons for editing entries/showing notes
- Create tooltips.
- Create a realtime tick-tock for the duration of the current item.
- Mobile version of the app.
- Refactor code, seperate out reusable bits for mobile version.
- Automatic code-highlighting (Highlight.js)
Done
====
- Add UI for note-taking
- Add Markdown converter for notes.
- Fix the duration bug when adding new events. Need to set the nextModel for
the previously 'current' timestamp and set the nextModel of the new timestamp
to 'null'

View File

@ -3,7 +3,7 @@
start() ->
ok = application:load(mnesia),
ok = application:set_env(mnesia, dir, "/home/jdbernard/projects/timestamper/web-app/db/test"),
ok = application:set_env(mnesia, dir, "/home/jdbernard/projects/jdb-labs/timestamper/web-app/db/test"),
ok = mnesia:start(),
error_logger:info_report("TimeStampter app started."),
ok.

View File

@ -34,19 +34,18 @@ out(YArg) ->
dispatch_request(YArg, _Session, []) -> make_json_404(YArg, [{see_docs, "/ts_api_doc"}]);
dispatch_request(YArg, Session, [H|T]) ->
Param = path_element_to_atom(H),
case {Session, Param} of
{_, login} -> do_login(YArg);
{_, logout} -> do_logout(YArg);
case {Session, H} of
{_, "login"} -> do_login(YArg);
{_, "logout"} -> do_logout(YArg);
{not_logged_in, _} -> make_json_401(YArg);
{session_expired, _} -> make_json_401(YArg, [{status, "session expired"}]);
{"not_logged_in", _} -> make_json_401(YArg);
{"session_expired", _} -> make_json_401(YArg, [{error, "session expired"}]);
{_S, app} -> dispatch_app(YArg, Session, T);
{_S, users} -> dispatch_user(YArg, Session, T);
{_S, timelines} -> dispatch_timeline(YArg, Session, T);
{_S, entries} -> dispatch_entry(YArg, Session, T);
{_S, "app"} -> dispatch_app(YArg, Session, T);
{_S, "users"} -> dispatch_user(YArg, Session, T);
{_S, "timelines"} -> dispatch_timeline(YArg, Session, T);
{_S, "entries"} -> dispatch_entry(YArg, Session, T);
{_S, _Other} -> make_json_404(YArg, [{see_docs, "/ts_api_doc/"}])
end.
@ -59,7 +58,7 @@ dispatch_app(YArg, Session, Params) ->
{'GET', ["user_summary", UsernameStr]} ->
case {Session#ts_api_session.username,
path_element_to_atom(UsernameStr)} of
UsernameStr} of
{Username, Username} -> get_user_summary(YArg, Username);
_ -> make_json_401(YArg)
@ -75,10 +74,9 @@ dispatch_app(YArg, Session, Params) ->
% -------- Dispatch for /user -------- %
dispatch_user(YArg, Session, []) ->
dispatch_user(YArg, Session, [atom_to_list(Session#ts_api_session.username)]);
dispatch_user(YArg, Session, [Session#ts_api_session.username]);
dispatch_user(YArg, Session, [H]) ->
Username = path_element_to_atom(H),
dispatch_user(YArg, Session, [Username]) ->
HTTPMethod = (YArg#arg.req)#http_request.method,
% compare to the logged-in user
@ -97,8 +95,7 @@ dispatch_user(YArg, Session, [H]) ->
dispatch_timeline(YArg, _Session, []) ->
make_json_404(YArg, [{see_docs, "/ts_api_doc/timelines.html"}]);
dispatch_timeline(YArg, Session, [UrlUsername|_T] = PathElements) ->
Username = path_element_to_atom(UrlUsername),
dispatch_timeline(YArg, Session, [Username|_T] = PathElements) ->
case Session#ts_api_session.username of
Username -> dispatch_timeline(YArg, PathElements);
@ -106,8 +103,7 @@ dispatch_timeline(YArg, Session, [UrlUsername|_T] = PathElements) ->
end.
% just username, list timelines
dispatch_timeline(YArg, [UrlUsername]) ->
Username = path_element_to_atom(UrlUsername),
dispatch_timeline(YArg, [Username]) ->
HTTPMethod = (YArg#arg.req)#http_request.method,
case HTTPMethod of
@ -115,9 +111,7 @@ dispatch_timeline(YArg, [UrlUsername]) ->
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines.html"}])
end;
dispatch_timeline(YArg, [UrlUsername, UrlTimelineId]) ->
Username = path_element_to_atom(UrlUsername),
TimelineId = path_element_to_atom(UrlTimelineId),
dispatch_timeline(YArg, [Username, TimelineId]) ->
HTTPMethod = (YArg#arg.req)#http_request.method,
case HTTPMethod of
@ -136,34 +130,29 @@ dispatch_timeline(YArg, _Other) ->
dispatch_entry(YArg, _Session, []) ->
make_json_404(YArg, [{see_docs, "/ts_aip_doc/entries.html"}]);
dispatch_entry(YArg, Session, [UrlUsername|_T] = PathElements) ->
Username = path_element_to_atom(UrlUsername),
dispatch_entry(YArg, Session, [Username|_T] = PathElements) ->
case Session#ts_api_session.username of
Username -> dispatch_entry(YArg, PathElements);
_Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/entries.html"}])
end.
dispatch_entry(YArg, [UrlUsername, UrlTimelineId]) ->
Username = path_element_to_atom(UrlUsername),
TimelineId = path_element_to_atom(UrlTimelineId),
dispatch_entry(YArg, [Username, TimelineId]) ->
HTTPMethod = (YArg#arg.req)#http_request.method,
case HTTPMethod of
'GET' -> list_entries(YArg, Username, TimelineId);
'PUT' -> put_entry(YArg, Username, TimelineId);
'POST' -> post_entry(YArg, Username, TimelineId);
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/entries.html"}])
end;
dispatch_entry(YArg, [UrlUsername, UrlTimelineId, UrlEntryId]) ->
Username = path_element_to_atom(UrlUsername),
TimelineId = path_element_to_atom(UrlTimelineId),
dispatch_entry(YArg, [Username, TimelineId, UrlEntryId]) ->
EntryId = list_to_integer(UrlEntryId), % TODO: catch non-numbers
HTTPMethod = (YArg#arg.req)#http_request.method,
case HTTPMethod of
'GET' -> get_entry(YArg, Username, TimelineId, EntryId);
'POST' -> post_entry(YArg, Username, TimelineId, EntryId);
'PUT' -> put_entry(YArg, Username, TimelineId, EntryId);
'DELETE' -> delete_entry(YArg, Username, TimelineId, EntryId);
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/entries.html"}])
end;
@ -184,8 +173,7 @@ do_login(YArg) ->
lists:keyfind(password, 1, Fields)} of
% username and password found
{{username, UnameField}, {password, Password}} ->
Username = list_to_atom(UnameField),
{{username, Username}, {password, Password}} ->
% check the uname, password
case ts_user:check_credentials(Username, Password) of
@ -200,7 +188,7 @@ do_login(YArg) ->
[CookieVal])}}];
% they are not good
false -> make_json_401(YArg, [{status,
false -> make_json_401(YArg, [{error,
"bad username/password combination"}])
end;
@ -224,8 +212,7 @@ get_user_summary(YArg, Username) ->
lists:map(fun ts_json:record_to_ejson/1, Timelines)},
JSONResp = json:encode({struct,
[{status, "ok"},
{user, EJSONUser},
[{user, EJSONUser},
{timelines, EJSONTimelines}
]}),
@ -262,9 +249,7 @@ list_timelines(YArg, Username) ->
EJSONTimelines = {array, lists:map(fun ts_json:record_to_ejson/1, Timelines)},
% create resposne
JSONResponse = json:encode({struct, [
{status, "ok"},
{timelines, EJSONTimelines}]}),
JSONResponse = json:encode(EJSONTimelines),
% return response
{content, "application/json", JSONResponse}.
@ -273,18 +258,22 @@ get_timeline(YArg, Username, TimelineId) ->
% look for timeline
case ts_timeline:lookup(Username, TimelineId) of
% no such timeline, return 404
no_record -> make_json_404(YArg, [{status, "no such timeline"}]);
no_record -> make_json_404(YArg, [{error, "no such timeline"}]);
% return the timeline data
Timeline -> make_json_200(YArg, Timeline)
end.
put_timeline(YArg, Username, TimelineId) ->
post_timeline(YArg, Username, TimelineId) ->
% parse the request body
EJSON = parse_json_body(YArg),
% parse into a Timeline record
TR = ts_json:ejson_to_record(#ts_timeline{}, EJSON),
TR = try ts_json:ejson_to_record(#ts_timeline{}, EJSON)
catch _:InputError ->
error_logger:error_report("Bad input: ~p", [InputError]),
throw(make_json_400(YArg))
end,
% set username and timeline id
NewRecord = TR#ts_timeline{ref = {Username, TimelineId}},
@ -297,12 +286,7 @@ put_timeline(YArg, Username, TimelineId) ->
% will not create, record exists
{error, {record_exists, ExistingRecord}} ->
EJSONRec = ts_json:record_to_ejson(ExistingRecord),
JSONResponse = json:encode({struct, [
{status, "ignored"},
{timeline, EJSONRec},
{see_docs, "/ts_api_doc/timelines.html#PUT"}
]}),
JSONResponse = json:encode(ts_json:record_to_ejson(ExistingRecord)),
{content, "application/json", JSONResponse};
@ -311,18 +295,22 @@ put_timeline(YArg, Username, TimelineId) ->
make_json_500(YArg, Error)
end.
post_timeline(YArg, Username, TimelineId) ->
put_timeline(YArg, Username, TimelineId) ->
% parse the POST data
EJSON = parse_json_body(YArg),
%{struct, Fields} = EJSON,
% parse into a timeline record
TR = ts_json:ejson_to_record(#ts_timeline{}, EJSON),
TR = try ts_json:ejson_to_record(#ts_timeline{}, EJSON)
catch _:InputError ->
error_logger:error_report("Bad input: ~p", [InputError]),
throw(make_json_400(YArg))
end,
% not supported right now, would require changing all the entry keys
% check to see if they are changing the timeline id
%NewTimelineId = case lists:keyfind(1, timeline_id, Fields) of
% {timeline_id, Field} -> list_to_atom(Field);
% {timeline_id, Field} -> Field;
% false -> TimelineId end,
% set username and timeline id
@ -332,8 +320,8 @@ post_timeline(YArg, Username, TimelineId) ->
ok -> make_json_200(YArg, NewRecord);
no_record -> make_json_404(YArg,
[{status, "no such timeline"},
{see_docs, "/ts_api_doc/timelines.html#POST"}]);
[{error, "no such timeline"},
{see_docs, "/ts_api_doc/timelines.html#PUT"}]);
Error ->
error_logger:error_report("Unable update timeline: ~p", [Error]),
@ -351,7 +339,7 @@ list_entries(YArg, Username, TimelineId) ->
lists:keyfind("byDate", 1, QueryData)} of
{no_record, _ByDateField} -> make_json_404(
[{status, "no such timeline"},
[{error, "no such timeline"},
{see_docs, "/ts_api_doc/entries.html#LIST"}]);
% listing by date range
@ -385,9 +373,7 @@ list_entries(YArg, Username, TimelineId) ->
EJSONEntries = {array, lists:map(
fun ts_json:record_to_ejson/1, Entries)},
JSONResponse = json:encode({struct, [
{status, "ok"},
{entries, EJSONEntries}]}),
JSONResponse = json:encode(EJSONEntries),
{content, "application/json", JSONResponse};
@ -419,9 +405,7 @@ list_entries(YArg, Username, TimelineId) ->
EJSONEntries = {array, lists:map(
fun ts_json:record_to_ejson/1, Entries)},
JSONResponse = json:encode({struct, [
{status, "ok"},
{entries, EJSONEntries}]}),
JSONResponse = json:encode(EJSONEntries),
{content, "application/json", JSONResponse}
end.
@ -429,18 +413,22 @@ list_entries(YArg, Username, TimelineId) ->
get_entry(YArg, Username, TimelineId, EntryId) ->
case ts_entry:lookup(Username, TimelineId, EntryId) of
% no such entry
no_record -> make_json_404(YArg, [{status, "no such entry"}]);
no_record -> make_json_404(YArg, [{error, "no such entry"}]);
% return the entry data
Entry -> make_json_200(YArg, Entry)
end.
put_entry(YArg, Username, TimelineId) ->
post_entry(YArg, Username, TimelineId) ->
% parse the request body
EJSON = parse_json_body(YArg),
% parse into ts_entry record
ER = ts_json:ejson_to_record(#ts_entry{}, EJSON),
ER = try ts_json:ejson_to_record(#ts_entry{}, EJSON)
catch _:InputError ->
error_logger:error_report("Bad input: ~p", [InputError]),
throw(make_json_400(YArg))
end,
% set username and timeline id
NewRecord = ER#ts_entry{ref = {Username, TimelineId, undef}},
@ -452,12 +440,7 @@ put_entry(YArg, Username, TimelineId) ->
% will not create, record exists
{error, {record_exists, ExistingRecord}} ->
EJSONRec = ts_json:record_to_ejson(ExistingRecord),
JSONResponse = json:encode({struct, [
{status, "ignored"},
{entry, EJSONRec},
{see_docs, "/ts_api_doc/entries.html#PUT"}
]}),
JSONResponse = json:encode(ts_json:record_to_ejson(ExistingRecord)),
{content, "application/json", JSONResponse};
@ -466,13 +449,17 @@ put_entry(YArg, Username, TimelineId) ->
make_json_500(YArg, OtherError)
end.
post_entry(YArg, Username, TimelineId, EntryId) ->
put_entry(YArg, Username, TimelineId, EntryId) ->
% parse the POST data
EJSON = parse_json_body(YArg),
% parse into ts_entry record
ER = ts_json:ejson_to_record(#ts_entry{}, EJSON),
ER = try ts_json:ejson_to_record(#ts_entry{}, EJSON)
catch _:InputError ->
error_logger:error_report("Bad input: ~p", [InputError]),
throw(make_json_400(YArg))
end,
% set uername, timeline id, and entry id
NewRecord = ER#ts_entry{ref = {Username, TimelineId, EntryId}},
@ -481,7 +468,7 @@ post_entry(YArg, Username, TimelineId, EntryId) ->
ok -> make_json_200(YArg, NewRecord);
no_record -> make_json_404(YArg,
[{status, "no such entry"}, {see_docs, "/ts_api_doc/entries.html#POST"}]);
[{error, "no such entry"}, {see_docs, "/ts_api_doc/entries.html#POST"}]);
Error ->
error_logger:error_report("TimeStamper: Unable to update entry: ~p", [Error]),
@ -509,10 +496,6 @@ delete_entry(YArg, Username, TimelineId, EntryId) ->
% ======== UTIL METHODS ======== %
% ============================== %
%% Convert one path element to an atom.
path_element_to_atom(PE) ->
list_to_atom(re:replace(PE, "\\s", "_", [{return, list}])).
parse_json_body(YArg) ->
case catch json:decode([], binary_to_list(YArg#arg.clidata)) of
{done, {ok, EJSON}, _} -> EJSON;
@ -524,17 +507,7 @@ parse_json_body(YArg) ->
%% Create a JSON 200 response.
make_json_200(_YArg, Record) ->
EJSONRecord = ts_json:record_to_ejson(Record),
Tag = case element(1, Record) of
ts_user -> user;
ts_timeline -> timeline;
ts_entry -> entry
end,
JSONResponse = json:encode({struct, [
{status, "ok"},
{Tag, EJSONRecord}
]}),
JSONResponse = json:encode(ts_json:record_to_ejson(Record)),
{content, "application/json", JSONResponse}.
make_json_400(YArg) -> make_json_400(YArg, []).
@ -574,19 +547,19 @@ make_json_405(YArg) -> make_json_405(YArg, []).
make_json_405(YArg, Fields) ->
% add default status if not provided
F1 = case lists:keyfind(status, 1, Fields) of
false -> Fields ++ [{status, method_not_allowed}];
false -> Fields ++ [{status, "method not allowed"}];
_Else -> Fields
end,
% add the path they requested
F2 = F1 ++ [{path, (YArg#arg.req)#http_request.path}],
% F2 = F1 ++ [{path, io_lib:format("~s", [(YArg#arg.req)#http_request.path])}],
[{status, 405}, {content, "application/json", json:encode({struct, F2})}].
[{status, 405}, {content, "application/json", json:encode({struct, F1})}].
make_json_500(_YArg, Error) ->
EJSON = {struct, [
{status, "internal server error"},
{error, io_lib:format("~p", [Error])}]},
{error, io_lib:format("~s", [Error])}]},
[{status, 500}, {content, "application/json", json:encode(EJSON)}].
make_json_500(_YArg) ->

View File

@ -4,8 +4,8 @@
pwd_salt,
name,
email,
join_date%,
% ext_data % other extensible data
join_date,
ext_data = [] % other extensible data
}).
% ts_user.ext_data can be:

View File

@ -7,10 +7,11 @@ encode_record(Record) -> lists:flatten(json:encode(record_to_ejson(Record))).
record_to_ejson(Record=#ts_user{}) ->
{struct, [
{username, atom_to_list(Record#ts_user.username)},
{id, Record#ts_user.username},
{name, Record#ts_user.name},
{email, Record#ts_user.email},
{join_date, encode_datetime(Record#ts_user.join_date)}]};
{join_date, encode_datetime(Record#ts_user.join_date)},
{ext_data, Record#ts_user.ext_data}]};
record_to_ejson(Record=#ts_timeline{}) ->
% pull out the username and timeline id
@ -18,8 +19,8 @@ record_to_ejson(Record=#ts_timeline{}) ->
% create the EJSON struct
{struct, [
{username, atom_to_list(Username)},
{timeline_id, atom_to_list(TimelineId)},
{user_id, Username},
{id, TimelineId},
{created, encode_datetime(Record#ts_timeline.created)},
{description, Record#ts_timeline.desc}]};
@ -32,15 +33,15 @@ record_to_ejson(Record=#ts_entry{}) ->
% create the EJSON struct
{struct, [
{username, atom_to_list(Username)},
{timeline_id, atom_to_list(TimelineId)},
{entry_id, EntryId},
{user_id, Username},
{timeline_id, TimelineId},
{id, EntryId},
{timestamp, encode_datetime(DateTime)},
{mark, Record#ts_entry.mark},
{notes, Record#ts_entry.notes}]}.
encode_datetime({{Year, Month, Day}, {Hour, Minute, Second}}) ->
lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B~3.10.0BZ",
lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B.~3.10.0BZ",
[Year, Month, Day, Hour, Minute, Second, 000])).
ejson_to_record(_Empty=#ts_timeline{}, EJSON) ->

View File

@ -2,261 +2,361 @@
* author: Jonathan Bernard
* TimeStamper main CSS for screen media types
*/
/*
$obg: #D9CEB2;
$bor: #948C75;
$ibg: #D5DED9;
$txt: #7A6A53;
$bbg: #99B2B7;
*/
/*
$obg: #979681;
$obor: #E6DEC7;
$ibg: #657A8B;
$bbor: #B34C2B;
$bbg: #252D42;
$txt: #E6DEC7;
*/
/* _rounded.scss */
html {
background: url("/img/loving_blu.png") repeat; }
* {
color: inherit; }
body {
width: 50%;
background-color: #657a8b;
color: #222222;
width: 75%;
margin: auto;
padding: 1em;
border: solid #b34c2b;
border-top: 0;
-moz-border-radius-bottomleft: 1em;
-webkit-border-bottom-left-radius: 1em;
border-bottom-left-radius: 1em;
-moz-border-radius-bottomright: 1em;
-webkit-border-bottom-right-radius: 1em;
border-bottom-right-radius: 1em; }
padding: 0; }
@media all and (min-device-width: 480) {
body {
width: 50%; } }
input {
border: solid thin #555555;
-webkit-box-shadow: inset 0px 2px 4px #CCC;
box-shadow: inset 0px 2px 4px #CCC;
margin: 0;
margin-bottom: 0.5em;
padding: 0;
font-family: Cantarell; }
@media all and (max-device-width: 480) {
body {
width: 80%; } }
.control-links {
color: #c5c5b9;
float: right;
display: block;
height: 100%;
font-weight: bold;
font-size: smaller; }
.control-links:hover {
color: #252d42; }
.control-links a {
color: inherit;
text-decoration: none;
margin-right: 0.5em; }
.control-links a:hover {
color: #b34c2b;
text-decoration: underline; }
.bar {
font-family: Helvetica, sans-serif;
color: #252d42;
background-color: #e6dec7;
border-color: #979681;
border-style: solid;
border-width: 0.2em;
border-bottom-width: 0;
padding: 0.1em 1em 0.1em 1em;
overflow: hidden; }
.last-bar {
border-color: #979681;
border-style: solid;
border-width: 0.2em;
-moz-border-radius-bottomright: 0.5em;
-webkit-border-bottom-right-radius: 0.5em;
border-bottom-right-radius: 0.5em;
-moz-border-radius-bottomleft: 0.5em;
-webkit-border-bottom-left-radius: 0.5em;
border-bottom-left-radius: 0.5em;
background-color: #e6dec7;
padding: 0.1em 1em 0.1em 1em;
overflow: hidden; }
#more-entries {
overflow: hidden; }
#more-entries div {
float: right;
left: -50%;
position: relative; }
#more-entries div a {
position: relative;
float: right;
left: 50%;
border: 1px solid #979681;
padding: 0.1em;
background: #f6f3ea;
color: #626150;
text-decoration: none;
font-size: smaller; }
#more-entries div a:hover {
color: #b34c2b; }
.bar form {
border-top: solid 1px #979681;
padding: 0.5em 0 0.5em 2em;
overflow: hidden; }
.bar form label {
color: #626150;
display: block;
overflow: hidden; }
.bar form label span {
float: left;
width: 6em;
padding-top: 0.1em; }
.bar form input.text-input {
border: 1px solid #979681; }
.bar form .form-col {
overflow: hidden;
float: left;
padding-right: 2em; }
.bar form .form-col input.text-input {
width: 10em; }
.bar form div.form-submit {
float: left;
width: 100%;
overflow: hidden;
padding: 0.5em 2em 0.5em 2em;
position: relative; }
.bar form div.form-submit div {
position: relative;
float: right;
left: -50%; }
.bar form div.form-submit div input {
position: relative;
left: 50%; }
.bar form div.form-submit input, .bar form input.form-submit {
border: 1px solid #979681;
background: #f6f3ea; }
#user {
-moz-border-radius-topleft: 0.5em;
-webkit-border-top-left-radius: 0.5em;
border-top-left-radius: 0.5em;
-moz-border-radius-topright: 0.5em;
-webkit-border-top-right-radius: 0.5em;
border-top-right-radius: 0.5em; }
#user .control-links {
padding-top: 0.6em; }
#user #fullname, #user #username {
font-weight: bold;
font-size: x-large;
float: left; }
#user #username {
padding-left: 0.2em;
color: #626150; }
#user #change-pwd {
display: none; }
#timeline #timeline-name, #timeline #timeline-desc {
font-weight: bold; }
#timeline #timeline-desc {
color: #626150; }
#timeline .control-links {
padding-top: 0.2em; }
#user-info, #timeline-info {
display: none;
width: 100%;
float: left; }
#new-entry {
-moz-border-radius-bottomright: 0.5em;
-webkit-border-bottom-right-radius: 0.5em;
border-bottom-right-radius: 0.5em;
-moz-border-radius-bottomleft: 0.5em;
-webkit-border-bottom-left-radius: 0.5em;
border-bottom-left-radius: 0.5em;
border-bottom: solid #979681 0.2em;
margin-bottom: 1em; }
#new-entry form {
border: 0;
#top {
background: #222222;
color: #eeeeee;
margin: 0;
opacity: 1;
padding: 0.5em 0;
padding: 0.5rem 0;
position: fixed;
top: 0px;
width: 75%;
z-index: 1; }
#top * {
margin: 0;
padding: 0; }
#new-entry input {
color: #626150; }
#new-entry #add-notes {
display: none;
padding: 0.5em 0 0.5em 2em; }
#new-entry #new-entry-input {
margin-right: 1em;
width: 15em; }
#top #fade-bar {
background: url("img/fade.png") repeat-x;
height: 32px;
width: 100%; }
.entry-bar {
background-color: #e6dec7;
border-color: #979681;
border-style: solid;
border-width: 0.2em;
border-bottom-width: 0;
padding: 0.1em 1em 0.1em 1em;
overflow: hidden; }
.entry-bar .id {
float: left;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
border-radius: 0.5em;
padding: 0 0.3em 0 0.3em;
background: #b34c2b;
color: #c5c5b9;
font-weight: bold;
width: 2em;
text-align: right; }
.entry-bar .details {
float: left; }
.entry-bar .details .entry-mark {
#timeline {
border-bottom: thin solid #eeeeee;
font-family: Arvo;
font-size: 1.5em;
padding: 0 2em;
padding: 0 2rem; }
#timeline .timeline-desc {
display: inline-block;
width: 70%; }
#timeline .timeline-desc-input {
width: 70%; }
#timeline .timeline-id {
display: inline-block; }
#timeline .timeline-desc-input, #timeline .timeline-id-input {
font-family: inherit;
font-size: inherit;
color: #222222;
display: none; }
#timeline.edit-id .timeline-id-input {
display: inline-block; }
#timeline.edit-id .timeline-id {
display: none; }
#timeline.edit-desc .timeline-desc-input {
display: inline-block; }
#timeline.edit-desc .timeline-desc {
display: none; }
#timeline .drop-menu {
text-align: right;
display: inline-block;
width: 29%; }
#timeline .drop-menu .drop-menu-items {
text-align: right;
right: 0;
width: 172.41%; }
#timeline .drop-menu .drop-menu-items .new-timeline-link, #timeline .drop-menu .drop-menu-items .timeline-link {
padding-left: 0.5em;
font-size: medium;
font-weight: bold;
font-family: Helvetica, sans-serif; }
.entry-bar .details .entry-notes {
display: none;
padding-left: 1.5em; }
.entry-bar .entry-edit {
display: none; }
.entry-bar .entry-edit .id {
width: 2em;
padding: 0.2em 0.5em 0.2em 0.5em; }
.entry-bar .entry-edit .entry-notes {
padding: 0; }
.top-entry {
-moz-border-radius-topleft: 0.5em;
-webkit-border-top-left-radius: 0.5em;
border-top-left-radius: 0.5em;
-moz-border-radius-topright: 0.5em;
-webkit-border-top-right-radius: 0.5em;
border-top-right-radius: 0.5em; }
#login-dialog {
font-size: small; }
#login-dialog input.text {
margin-bottom: 1em;
width: 95%;
padding: 0.4em; }
#login-dialog form fieldset {
padding: 0;
border: 0;
margin: 0; }
#login-dialog .validate-tips {
display: none; }
#ui-dialog-title-login-dialog, .ui-dialog-buttonset {
font-size: medium; }
#timeline .drop-menu .drop-menu-items .new-timeline-link img, #timeline .drop-menu .drop-menu-items .timeline-link img {
position: relative;
top: 4px;
left: -4px; }
.signup {
.dialog {
background: white;
background: rgba(255, 255, 255, 0.5);
color: #eeeeee;
margin: 0;
padding: 0;
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100em;
z-index: 10; }
.dialog * {
margin: 0;
padding: 0; }
.dialog div.container {
background: #222222;
border-radius: 10px;
font-family: Cantarell;
margin-left: auto;
margin-right: auto;
margin-top: 4em;
padding: 1em;
width: 20em; }
.dialog div.container h2 {
border-bottom: thin solid #eeeeee;
font-family: Arvo;
padding-bottom: 0.5em;
margin-bottom: 0.5em; }
.dialog div.container label {
display: inline-block;
width: 6em; }
.dialog div.container input {
color: #222222; }
.dialog div.container .button-panel {
margin-top: 0.5em;
overflow: hidden; }
.dialog div.container .button-panel .validate-tips {
font-size: 1em; }
.dialog div.container .button-panel .dialog-button {
float: right;
padding-left: 1em;
font-family: Arvo;
font-size: 1.2em; }
.dialog div.container .button-panel .dialog-button a {
color: #eeeeee; }
#login.dialog {
background: white;
opacity: 1; }
/*#login {
background: white;
color: $lightTxt;
margin: 0;
padding: 0;
opacity: 1;
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100em;
z-index: 10;
* {
margin: 0;
padding: 0;
}
div.container {
background: $darkBg;
border-radius: 10px;
font-family: Cantarell;
margin-left: auto;
margin-right: auto;
margin-top: 4em;
padding: 1em;
width: 20em;
h2 {
border-bottom: thin solid $lightBg;
font-family: Arvo;
padding-bottom: 0.5em;
margin-bottom: 0.5em;
}
label {
display: inline-block;
width: 6em;
}
input {
color: $darkTxt;
}
#login-button {
text-align: right;
margin-top: 0.5em;
font-family: Arvo;
font-size: 1.2em;
.validate-tips { font-size: 1em; }
a { color: $lightBg; }
}
}
}*/
#user {
font-family: "Josefin Sans";
margin-top: -0.3em;
padding: 0 2em;
margin-top: -0.3rem;
padding: 0 2rem;
width: 100%; }
#user .fullname, #user .username {
display: inline-block;
font-size: larger; }
#user .fullname-input {
font-family: inherit;
font-size: inherit;
color: #222222;
display: none; }
#user.edit-fullname .fullname-input {
display: inline-block; }
#user.edit-fullname .fullname {
display: none; }
#user .user-menu {
display: inline-block; }
#user .user-menu .user-menu-items {
list-style: none;
display: none; }
#user .user-menu .user-menu-items li {
display: inline-block;
padding-left: 0.5em; }
#user .user-menu .user-menu-items a {
text-decoration: none; }
#user .user-menu .user-menu-items a:hover {
text-decoration: underline; }
#user .user-menu:hover .user-menu-items, #user .user-menu-items:hover {
display: inline-block; }
#entry-list {
margin: 6em 0 0 0;
margin: 6rem 0 0 0;
padding-bottom: 1em 0 0 0;
padding-bottom: 1rem 0 0 0; }
#entry-list .day-seperator {
background: #cccccc;
color: #222222;
font-family: Cantarell;
font-weight: bold;
margin: 1em 0 0 0;
margin: 1rem 0 0 0;
padding: 0 2em;
padding: 0 2rem; }
#entry-list .day-seperator * {
margin: 0;
padding: 0; }
#entry-list .day-seperator h4, #entry-list .day-seperator h5 {
display: inline-block; }
#entry-list .day-seperator h5 {
color: #667; }
#entry-list #new-entry {
margin: 0.5em 0 0 0;
padding: 0 2em;
margin: 0.5rem 0 0 0;
padding: 0 2rem; }
#entry-list #new-entry * {
margin: 0;
padding: 0; }
#entry-list .timestamp, #entry-list .timestamp-input, #entry-list .duration {
text-align: right;
width: 14%; }
#entry-list .mark, #entry-list .mark-input {
width: 70%; }
#entry-list .entry {
font-family: Cantarell;
padding: 0 2em;
padding: 0 2rem; }
#entry-list .entry div {
display: inline-block; }
#entry-list .entry .mark {
margin: 0;
padding: 0;
position: relative; }
#entry-list .entry .mark * {
margin: 0;
padding: 0; }
#entry-list .entry .mark img.expand-entry, #entry-list .entry .mark img.collapse-entry {
display: none;
left: -20px;
position: absolute;
top: 6px; }
#entry-list .entry:hover .mark img.expand-entry, #entry-list .entry.show-notes img.collapse-entry {
display: inline; }
#entry-list .entry .mark-input, #entry-list .entry .timestamp-input, #entry-list .entry.show-notes:hover img.expand-entry {
display: none; }
#entry-list .entry .notes {
display: none;
font-family: Cantarell;
font-size: small;
margin: 0;
padding: 0 0 0 1em; }
#entry-list .entry .notes :first-child {
margin-top: 0; }
#entry-list .entry .notes .notes-input, #entry-list .entry .notes pre, #entry-list .entry .notes code {
font-family: 'Anonymous Pro'; }
#entry-list .entry .notes * {
width: 100%; }
#entry-list .entry.edit-mark .mark-input {
display: inline-block; }
#entry-list .entry.edit-mark .mark {
display: none; }
#entry-list .entry.edit-timestamp .timestamp-input {
display: inline-block; }
#entry-list .entry.edit-timestamp .timestamp {
display: none; }
#entry-list .entry .notes-input {
display: none; }
#entry-list .entry.edit-notes .notes-input {
display: block; }
#entry-list .entry.edit-notes .notes-text {
display: none; }
#signup-checkbox {
display: inline; }
.drop-menu {
margin: 0;
padding: 0;
position: relative; }
.drop-menu * {
margin: 0;
padding: 0; }
.drop-menu .drop-menu-items {
display: none;
list-style: none;
position: absolute; }
.drop-menu .drop-menu-items li {
display: inline-block;
padding-left: 0.5em; }
.drop-menu:hover .drop-menu-items, .drop-menu .drop-menu-items:hover {
display: block; }
.drop-menu a {
display: inline-block;
text-decoration: none; }
.drop-menu a:hover {
text-decoration: underline; }
.footer {
background: #222222;
color: #eeeeee;
font-family: Bentham;
margin: 0;
padding: 1em 0;
padding: 1rem 0;
text-align: center;
width: 100%; }
.footer * {
margin: 0;
padding: 0; }
.footer a {
color: white;
text-decoration: none; }
.footer a:hover {
text-decoration: underline; }
.logo {
font-family: Bentham;
text-decoration: overline underline;
color: inherit; }
.hidden {
display: none; }

View File

@ -3,328 +3,474 @@
* TimeStamper main CSS for screen media types
*/
/*
$obg: #D9CEB2;
$bor: #948C75;
$ibg: #D5DED9;
$txt: #7A6A53;
$bbg: #99B2B7;
*/
/*
$obg: #979681;
$obor: #E6DEC7;
$ibg: #657A8B;
$bbor: #B34C2B;
$bbg: #252D42;
$txt: #E6DEC7;
*/
$obg: #252D42;
$obor: #B34C2B;
$ibg: #657A8B;
$bbor: #979681;
$bbg: #E6DEC7;
$txt: #252D42;
$greyTxt: darken($bbor, 20%);
$iBorWidth: 0.2em;
@import "rounded";
html {
//background-color: $obg;
background: url('/img/loving_blu.png') repeat;
$darkTxt: #222;
$lightTxt: #eee;
$darkBg: #222;
$lightBg: #eee;
$medBg: #CCC;
* {
color: inherit;
}
body {
width: 50%;
background-color: $ibg;
color: $darkTxt;
width: 75%;
margin: auto;
padding: 1em;
border: solid $obor;
border-top: 0;
@include rounded2(bottom, left, 1em);
@include rounded2(bottom, right, 1em);
padding: 0;
}
@media all and (min-device-width: 480) { body { width: 50%; }}
@media all and (max-device-width: 480) { body { width: 80%; }}
.control-links {
color: lighten($greyTxt, 40%);
float: right;
display: block;
height: 100%;
font-weight: bold;
font-size: smaller;
&:hover { color: $txt; }
a {
color: inherit; //lighten($greyTxt, 20%);
text-decoration: none;
margin-right: 0.5em;
&:hover {
color: $obor;
text-decoration: underline;
}
}
input {
border: solid thin lighten($darkTxt, 20%);
-webkit-box-shadow: inset 0px 2px 4px #CCC;
box-shadow: inset 0px 2px 4px #CCC;
margin: 0;
margin-bottom: 0.5em; // IE fix
padding: 0;
font-family: Cantarell;
}
.bar {
#top {
background: $darkBg;
color: $lightTxt;
margin: 0;
opacity: 1;
padding: 0.5em 0; // IE Fix
padding: 0.5rem 0;
position: fixed;
top: 0px;
width: 75%;
z-index: 1;
font-family: Helvetica, sans-serif;
color: $txt;
background-color: $bbg;
border-color: $bbor;
border-style: solid;
border-width: $iBorWidth;
border-bottom-width: 0;
padding: 0.1em 1em 0.1em 1em;
overflow: hidden;
}
.last-bar {
border-color: $bbor;
border-style: solid;
border-width: $iBorWidth;
@include rounded2(bottom, right, 0.5em);
@include rounded2(bottom, left, 0.5em);
background-color: $bbg;
padding: 0.1em 1em 0.1em 1em;
overflow: hidden;
}
#more-entries {
overflow: hidden;
div {
float: right;
left: -50%;
position: relative;
a {
position: relative;
float: right;
left: 50%;
border: 1px solid $bbor;
padding: 0.1em;
background: lighten($bbg, 10%);
color: $greyTxt;
text-decoration: none;
font-size: smaller;
&:hover { color: $obor; }
}
}
}
.bar form {
border-top: solid 1px $bbor;
padding: 0.5em 0 0.5em 2em;
overflow: hidden;
label {
span {
float: left;
width: 6em;
padding-top: 0.1em;
}
color: $greyTxt;
display: block;
overflow: hidden;
* {
margin: 0;
padding: 0;
}
input.text-input { border: 1px solid $bbor; }
.form-col {
overflow: hidden;
float: left;
padding-right: 2em;
input.text-input { width: 10em; }
}
div.form-submit {
float: left;
#fade-bar {
background: url('img/fade.png') repeat-x;
height: 32px;
width: 100%;
overflow: hidden;
padding: 0.5em 2em 0.5em 2em;
position: relative;
div {
position: relative;
float: right;
left: -50%;
input {
position: relative;
left: 50%;
}
}
}
div.form-submit input, input.form-submit {
border: 1px solid $bbor;
background: lighten($bbg, 10%);
}
}
#user {
@include rounded2(top, left, 0.5em);
@include rounded2(top, right, 0.5em);
.control-links { padding-top: 0.6em; }
#fullname, #username {
font-weight: bold;
font-size: x-large;
float: left;
}
#username {
padding-left: 0.2em;
color: $greyTxt;
}
#change-pwd { display: none; }
#user-info .form-submit {
}
}
#timeline {
#timeline-name, #timeline-desc { font-weight: bold; }
border-bottom: thin solid $lightBg;
font-family: Arvo;
font-size: 1.5em;
padding: 0 2em;
padding: 0 2rem;
#timeline-desc { color: $greyTxt; }
.timeline-desc {
display: inline-block;
width: 70%;
}
.control-links { padding-top: 0.2em; }
.timeline-desc-input { width: 70% }
}
.timeline-id { display: inline-block; }
#user-info, #timeline-info {
.timeline-desc-input, .timeline-id-input {
font-family: inherit;
font-size: inherit;
color: $darkBg;
display: none;
width: 100%;
float: left;
}
&.edit-id {
.timeline-id-input { display: inline-block; }
.timeline-id { display: none; }
}
&.edit-desc {
.timeline-desc-input { display: inline-block; }
.timeline-desc { display: none; }
}
.drop-menu {
text-align: right;
display: inline-block;
width: 29%;
.drop-menu-items {
text-align: right;
right: 0;
width: 172.41%;
.new-timeline-link, .timeline-link {
padding-left: 0.5em;
font-size: medium;
img {
position: relative;
top: 4px;
left: -4px;
}
}
}
}
}
#new-entry {
.dialog {
@include rounded2(bottom, right, 0.5em);
@include rounded2(bottom, left, 0.5em);
border-bottom: solid $bbor $iBorWidth;
background: white;
background: rgba(255, 255, 255, 0.5);
color: $lightTxt;
margin: 0;
padding: 0;
margin-bottom: 1em;
position: fixed;
top: 0px;
left: 0px;
form {
border: 0;
width: 100%;
height: 100em;
z-index: 10;
* {
margin: 0;
padding: 0;
}
input { color: $greyTxt; }
div.container {
#add-notes {
display: none;
padding: 0.5em 0 0.5em 2em;
background: $darkBg;
border-radius: 10px;
font-family: Cantarell;
margin-left: auto;
margin-right: auto;
margin-top: 4em;
padding: 1em;
width: 20em;
h2 {
border-bottom: thin solid $lightBg;
font-family: Arvo;
padding-bottom: 0.5em;
margin-bottom: 0.5em;
}
#new-entry-input {
margin-right: 1em;
width: 15em;
label {
display: inline-block;
width: 6em;
}
input {
color: $darkTxt;
}
.button-panel {
margin-top: 0.5em;
overflow: hidden;
.validate-tips { font-size: 1em; }
.dialog-button {
float: right;
padding-left: 1em;
font-family: Arvo;
font-size: 1.2em;
a { color: $lightBg; }
}
}
}
}
.entry-bar {
#login.dialog {
background: white;
opacity: 1;
}
background-color: $bbg;
border-color: $bbor;
border-style: solid;
border-width: $iBorWidth;
border-bottom-width: 0;
padding: 0.1em 1em 0.1em 1em;
overflow: hidden;
/*#login {
background: white;
color: $lightTxt;
margin: 0;
padding: 0;
opacity: 1;
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100em;
z-index: 10;
* {
margin: 0;
padding: 0;
}
div.container {
background: $darkBg;
border-radius: 10px;
font-family: Cantarell;
margin-left: auto;
margin-right: auto;
margin-top: 4em;
padding: 1em;
width: 20em;
h2 {
border-bottom: thin solid $lightBg;
font-family: Arvo;
padding-bottom: 0.5em;
margin-bottom: 0.5em;
}
label {
display: inline-block;
width: 6em;
}
input {
color: $darkTxt;
}
#login-button {
.id {
float: left;
@include rounded(0.5em);
padding: 0 0.3em 0 0.3em;
background: $obor;
color: lighten($greyTxt, 40%);
font-weight: bold;
width: 2em;
text-align: right;
margin-top: 0.5em;
font-family: Arvo;
font-size: 1.2em;
.validate-tips { font-size: 1em; }
a { color: $lightBg; }
}
}
}*/
#user {
font-family: "Josefin Sans";
margin-top: -0.3em; // IE fix
padding: 0 2em;
margin-top: -0.3rem;
padding: 0 2rem;
width: 100%;
.fullname, .username {
display: inline-block;
font-size: larger;
}
.details {
.fullname-input {
font-family: inherit;
font-size: inherit;
color: $darkBg;
display: none;
}
float: left;
&.edit-fullname{
.fullname-input { display: inline-block; }
.fullname { display: none; }
}
.entry-mark {
.user-menu { display: inline-block; }
.user-menu .user-menu-items {
list-style: none;
display: none;
li {
display: inline-block;
padding-left: 0.5em;
font-size: medium;
}
a { text-decoration: none; }
a:hover { text-decoration: underline; }
}
.user-menu:hover .user-menu-items, .user-menu-items:hover { display: inline-block; }
}
#entry-list {
margin: 6em 0 0 0;
margin: 6rem 0 0 0;
padding-bottom: 1em 0 0 0;
padding-bottom: 1rem 0 0 0;
.day-seperator {
background: $medBg;
color: $darkBg;
font-family: Cantarell;
font-weight: bold;
font-family: Helvetica, sans-serif;
}
.entry-notes {
display: none;
padding-left: 1.5em;
}
margin: 1em 0 0 0;
margin: 1rem 0 0 0;
padding: 0 2em;
padding: 0 2rem;
* {
margin: 0;
padding: 0;
}
.entry-edit {
display: none;
h4, h5 { display: inline-block; }
.id {
width: 2em;
padding: 0.2em 0.5em 0.2em 0.5em;
h5 { color: #667; }
}
.entry-notes {
#new-entry {
margin: 0.5em 0 0 0;
padding: 0 2em;
margin: 0.5rem 0 0 0;
padding: 0 2rem;
* {
margin: 0;
padding: 0;
}
}
}
.top-entry {
.timestamp, .timestamp-input, .duration {
text-align: right;
width: 14%;
}
@include rounded2(top, left, 0.5em);
@include rounded2(top, right, 0.5em);
}
.mark, .mark-input { width: 70%; }
#login-dialog {
.entry {
font-family: Cantarell;
padding: 0 2em;
padding: 0 2rem;
div { display: inline-block; }
.mark {
margin: 0;
padding: 0;
position: relative;
* {
margin: 0;
padding: 0;
}
img.expand-entry, img.collapse-entry {
display: none;
left: -20px;
position: absolute;
top: 6px;
}
}
&:hover .mark img.expand-entry, &.show-notes img.collapse-entry { display: inline; }
.mark-input, .timestamp-input,
&.show-notes:hover img.expand-entry { display: none; }
.notes {
display: none;
font-family: Cantarell;
font-size: small;
margin: 0;
padding: 0 0 0 1em;
//label, input { display: block }
input.text {
margin-bottom: 1em;
width: 95%;
padding: 0.4em;
:first-child { margin-top: 0; }
.notes-input, pre, code { font-family: 'Anonymous Pro'; }
}
form fieldset {
padding: 0; border: 0; margin: 0;
.notes * { width: 100%; }
&.edit-mark {
.mark-input { display: inline-block; }
.mark { display: none; }
}
&.edit-timestamp {
.timestamp-input { display: inline-block; }
.timestamp { display: none; }
}
.notes-input { display: none; }
&.edit-notes .notes-input { display: block; }
&.edit-notes .notes-text { display: none; }
}
.validate-tips { display: none; }
}
#ui-dialog-title-login-dialog, .ui-dialog-buttonset {
font-size: medium;
.drop-menu {
margin: 0;
padding: 0;
position: relative;
* {
margin: 0;
padding: 0;
}
.drop-menu-items {
display: none;
list-style: none;
position: absolute;
li {
display: inline-block;
padding-left: 0.5em;
}
}
&:hover .drop-menu-items, .drop-menu-items:hover { display: block; }
a {
display: inline-block;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.signup { display: none; }
.footer {
#signup-checkbox { display: inline; }
background: $darkBg;
color: $lightTxt;
font-family: Bentham;
margin: 0;
padding: 1em 0;
padding: 1rem 0;
text-align: center;
width: 100%;
* {
margin: 0;
padding: 0;
}
a {
color: lighten($lightTxt, 20%);
text-decoration: none;
&:hover { text-decoration: underline; }
}
}
.logo {
font-family: Bentham;
text-decoration: overline underline;
color: inherit;
}
.hidden { display: none; }

BIN
www/img/br_down_icon_16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
www/img/br_down_icon_24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
www/img/br_down_icon_32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
www/img/br_down_icon_48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
www/img/br_up_icon_16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
www/img/br_up_icon_24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
www/img/br_up_icon_32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
www/img/br_up_icon_48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
www/img/pencil_icon&48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
www/img/pencil_icon_16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
www/img/pencil_icon_24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
www/img/pencil_icon_32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,11 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>TimeStamper - Simple Time Tracking</title>
<link rel="stylesheet" media="screen" href="/css/dot-luv/jquery-ui-1.8.10.custom.css" type="text/css"/>
<link href='http://fonts.googleapis.com/css?family=Anonymous+Pro|Arvo|Bentham|Cantarell|Josefin+Sans' rel='stylesheet' type='text/css'>
<link rel="stylesheet" media="screen" href="/css/ts-screen.css" type="text/css"/>
<!-- Needed for IE, but I'm not going to support IE with this tool. -->
<!-- Needed for IE, but I'm not sure if I'm going to support IE with this tool. -->
<!--<script type="text/javascript" src="/js/json2.js"></script>-->
<!-- PROD -->
<!--
@ -13,195 +12,156 @@
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.10/jquery-ui.min.js"></script>
-->
<!-- DEV -->
<script type="text/javascript" src="/js/jquery-1.5.min.js"></script>
<script type="text/javascript" src="/js/jquery-ui-1.8.10.custom.min.js"></script>
<script type="text/javascript" src="/js/jquery-1.5.js"></script>
<script type="text/javascript" src="/js/underscore-min.js"></script>
<script type="text/javascript" src="/js/showdown.js"></script>
<script type="text/javascript" src="/js/underscore.js"></script>
<script type="text/javascript" src="/js/ICanHaz.js"></script>
<script type="text/javascript" src="/js/backbone.js"></script>
<script type="text/javascript" src="/js/ts.js"></script>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<script type="text/javascript">
<erl>
out(YArg) ->
Session = ts_api_session:get_session(YArg),
case Session of not_logged_in -> {html, "//not logged in"}; session_expired -> {html, "//session expired"};
_S ->
Username = element(2, Session),
<script id="entry" type="text/html">
<div class="entry-bar" id="entry-{{entry_id}}">
<div class="entry-display">
<span class="id">{{entry_id}}</span>
<div class="details">
<div class="entry-mark">{{mark}}</div>
<div class="entry-notes">{{notes}}</div>
</div>
<div class="control-links">
<a onclick="$('#entry-{{entry_id}} .entry-display .entry-notes').slideToggle('slow');"
href="#">show notes</a>
<a onclick="toggleEditEntry(event, {{entry_id}})"
href="#">edit</a>
<a onclick="deleteEntry(event, {{entry_id}})"
href="#">del</a>
</div>
</div>
<div class="entry-edit">
<form action="/ts/update-entry.yaws"
onsubmit="updateEntry(event, {{entry_id}})">
<input type="text" id="entry-{{entry_id}}-id-input"
class="id" value="{{entry_id}}"/>
<div class="details">
<input type="text" id="entry-{{entry_id}}-mark-input"
class="entry-mark" value="{{mark}}"/></br>
<textarea id="entry-{{entry_id}}-notes-input"
class="entry-notes" rows="8" cols="40" >{{notes}}</textarea>
</div>
</form>
<div class="control-links">
<a onclick="$('#entry-{{entry_id}} .entry-edit .entry-notes').slideToggle('slow');"
href="#">show notes</a>
<a onclick="updateEntry(event, {{entry_id}})"
href="#">save changes</a>
</div>
</div>
% get the user
{content, _, UserJSON} = ts_api:get_user(YArg, Username),
UserRecord = ts_user:lookup(Username),
% get the timelines
{content, _, TimelineListJSON} = ts_api:list_timelines(YArg, Username),
% get the selected timeline
SelectedTimeline = case lists:keyfind(
selected_timeline, 1, element(8, UserRecord)) of
false -> ts_timeline:list(Username, 0, 1);
T -> T
end,
% get entries for this timeline
{content, _, EntryListJSON} =
ts_api:list_entries(YArg, Username, SelectedTimeline),
{html, f(
"function bootstrap() {~n"
" var data = {};~n"
" data.user = ~p;~n"
" data.timelines = ~p;~n"
" data.initialTimelineId = ~p;~n"
" data.entries = ~p;~n"
" return data;~n"
"};",
[UserJSON, TimelineListJSON, SelectedTimeline, EntryListJSON])}
end.
</erl>
</script>
<script type="text/html" id="userTemplate">
<div class="fullname">{{name}}</div>
<input class="fullname-input" type="text" value="{{name}}"/></div>
<div class="user-menu">
<div class="username"> - {{id}}</div>
<ul class="user-menu-items">
<li><a href="#">logout</a></li>
<li><a href="#">user info</a></li>
</ul>
</div>
</script>
<script type="text/html" id="timelineTemplate">
<span class="timeline-desc">{{description}}</span>
<input class="timeline-desc-input" type="text" value='{{description}}'/>
<div class="drop-menu">
<div class="timeline-id">(&nbsp;{{id}}&nbsp;)</div>
<input class="timeline-id-input" type="text" value='{{id}}'/>
<ul class="drop-menu-items">
<li class="new-timeline-link"><a href="#">
<img src="/img/round_plus_icon_16.png"/>new</a></li>
</ul>
</div>
</script>
<script type="text/html" id="timelineLinkTemplate">
<li class="timeline-link"><a href="#">{{id}}</a></li>
</script>
<script type="text/html" id="entryTemplate">
<div class="mark">
<img class="expand-entry" src="/img/br_down_icon_16.png"/>
<img class="collapse-entry" src="/img/br_up_icon_16.png"/>
<span>{{mark}}</span>
</div>
<input class="mark-input" type="text" value="{{mark}}"/>
<div class="timestamp">{{start}}</div>
<input class="timestamp-input" type="text" value="{{timestamp}}"/>
<div class="duration">{{duration}}</div>
<div class="notes">
<div class="notes-text">{{notes}}</div>
<textarea class="notes-input" rows="10">{{notes}}</textarea>
</div>
</script>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
</head>
<body>
<div id="user" class="bar">
<span id="fullname">Not Logged In</span>
<span id="username">- no_user</span>
<div class="control-links">
<a href="/ts/edit-user.yaws"
onclick="$('#user-info').slideToggle('slow'); return false;">
user info</a>
<a href="/ts/logout.yaws" onclick="logout(event)">logout</a>
<!-- == LOGIN FORM == -->
<div id="login" class="hidden dialog">
<div class="container">
<h2>Login</h2>
<div><label>Username: </label><input type="text" id="username-input"></input></div>
<div><label>Password: </label><input type="password" id="password-input"></input></div>
<div class="button-panel">
<span class='validate-tips'></span>
<div id="login-button" class="dialog-button"><a href="#">login</a></div>
</div>
<div id="user-info">
<form action="/ts/update-user.yaws" onsubmit="updateUser(event)">
<div class="form-col">
<label for="fullname-input"><span>name:</span>
<input id="fullname-input" name="fullname"
class="text-input" type="text"/>
</label>
<label for="email-input"><span>email:</span>
<input id="email-input" name="email"
class="text-input" type="text"/>
</label>
</div>
<div class="form-col">
<div id="change-pwd">
<label for="old-pwd-input"><span>password:</span>
<input id="old-pwd-input" name="old-pwd"
class="text-input" type="password"/>
</label>
<label for="new-pwd-input"><span>new pwd:</span>
<input id="new-pwd-input" name="new-pwd"
class="text-input" type="password"/>
</label>
<label for="new-pwd-conf-input"><span>confirm:</span>
<input id="new-pwd-conf-input" name="new-pwd-conf"
class="text-input" type="password"/>
</label>
</div>
<label for="enable-pwd-change-input">
<input name="enable-pwd-change" type="checkbox"
id="enable-pwd-change-input"
onclick="$('#change-pwd').slideToggle('slow');"/>
change password
</label>
</div>
<div class="form-submit">
<div>
<input name="submit-user" type="submit"
value="save changes"/>
</div>
</div>
</form>
</div>
</div>
<div id="timeline" class="bar">
<span id="timeline-name">timeline |</span>
<span id="timeline-desc">timeline description</span>
<div class="control-links">
<a href="/ts/edit-timeline.yaws"
onclick="$('#timeline-info').slideToggle('slow'); return false;">
timeline info</a>
<a href="/ts/select-timeline.yaws"
onclick="showTimelineMenu(event)">change timelines</a>
<!-- == NEW TIMELINE FORM == -->
<div id="new-timeline" class="hidden dialog">
<div class="container">
<h2>Create a new timeline:</h2>
<div><label>Timeline ID: </label><input type="text" id="new-timeline-id"></input></div>
<div><label>Description: </label><input type="text" id="new-timeline-desc"></input></div>
<div class="button-panel">
<span class='validate-tips'></span>
<div id="new-timeline-create" class="dialog-button"><a href="#">create</a></div>
<div id="new-timeline-cancel" class="dialog-button"><a href="#">cancel</a></div>
</div>
<div id="timeline-info">
<form action="/ts/update-timeline.yaws"
onsubmit="updateTimeline(event); false">
<label for="timeline-desc-input"><span>description:</span>
<input id="timeline-desc-input" class="text-input"
name="timeline-desc" type="text"/>
</label>
<div class="form-submit">
<div><input name="submit-timeline" type="submit"
value="save changes"/></div>
</div>
</form>
</div>
</div>
<div id="new-entry" class="bar">
<form action="/ts/new-entry.yaws" onsubmit="newEntry(event)">
begin a new activity:
<input name="new-entry" id="new-entry-input"
class="text-input" type="text"/>
<input name="submit-entry" id="submit-entry"
class="form-submit" type="submit" value="create entry"/>
<div class="control-links">
<a id="show-notes" href="#"
onclick="$('#add-notes').slideToggle('slow');">
add notes</a>
</div>
<div id="add-notes" class="form-col">
<label for="new-notes-input">notes:</label>
<textarea name="new-notes" id="new-notes-input"
class="text-input" rows="8" cols="40" ></textarea>
</div>
</form>
<div id="top">
<!-- == TIMELINE == -->
<div id="timeline"><!-- replaced on login by app -->
<div class="timeline-desc">Login</div>
</div>
<div id="more-entries" class="last-bar top-entry">
<div>
<a href="#" onclick="loadEntries(user, activeTimeline, 'old');event.preventDefault()">load more entries</a>
</div>
<!-- == USER == -->
<div id="user"><!-- replaced on login by app -->
</div>
<div id="login-dialog" title="Login">
<form>
<fieldset>
<label for="login-name">Username:</label>
<input type="text" name="login-name" id="login-name"
class="text ui-widget-content ui-corner-all"></input>
<div class="signup">
<label for="signup-fullname">Full name:</label>
<input type="text" name="signup-fullname" id="signup-fullname"
class="text ui-widget-content ui-corner-all"></input>
<label for="signup-email">eMail address:</label>
<input type="text" name="signup-email" id="signup-email"
class="text ui-widget-content ui-corner-all"></input>
</div>
<label for="login-password">Password:</label>
<input type="password" name="login-password" id="login-password"
class="text ui-widget-content ui-corner-all"></input>
<div class="signup">
<label for="confirm-password">Confirm password:</label>
<input type="password" name="confirm-password" id="confirm-password"
class="text ui-widget-content ui-corner-all"></input>
<div id="entry-list">
<div class="day-seperator">
<h4 class="mark">Today</h4>
<h5 class="timestamp">start</h5>
<h5 class="duration">duration</h5>
</div>
<div id="new-entry">
<input id="new-entry-input" class="mark-input"
placeholder="Start a new task..." type="text" />
</div>
<label for="signup-checkbox">
<input type="checkbox" id="signup-checkbox" name="signup-checkbox"
onclick="toggleSignUp(event)"/>
I'm a new user!
</label>
</fieldset>
</form>
<p class="validate-tips"></p>
<div id="entries"></div>
</div>
<div class="footer">
Copyright 2011 <a href="http://www.jdb-labs.com"><span class="logo">JDB Labs</span> LLC.</a>
</div>
</body>
</html>

27
www/js/backbone-min.js vendored Normal file
View File

@ -0,0 +1,27 @@
// Backbone.js 0.3.3
// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://documentcloud.github.com/backbone
(function(){var e;e=typeof exports!=="undefined"?exports:this.Backbone={};e.VERSION="0.3.3";var f=this._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var h=this.jQuery||this.Zepto;e.emulateHTTP=false;e.emulateJSON=false;e.Events={bind:function(a,b){this._callbacks||(this._callbacks={});(this._callbacks[a]||(this._callbacks[a]=[])).push(b);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=0,g=c.length;d<g;d++)if(b===c[d]){c.splice(d,
1);break}}else c[a]=[]}else this._callbacks={};return this},trigger:function(a){var b,c,d,g;if(!(c=this._callbacks))return this;if(b=c[a]){d=0;for(g=b.length;d<g;d++)b[d].apply(this,Array.prototype.slice.call(arguments,1))}if(b=c.all){d=0;for(g=b.length;d<g;d++)b[d].apply(this,arguments)}return this}};e.Model=function(a,b){a||(a={});if(this.defaults)a=f.extend({},this.defaults,a);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.set(a,{silent:true});this._previousAttributes=
f.clone(this.attributes);if(b&&b.collection)this.collection=b.collection;this.initialize(a,b)};f.extend(e.Model.prototype,e.Events,{_previousAttributes:null,_changed:false,initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.attributes[a];return this._escapedAttributes[a]=(b==null?"":b).replace(/&(?!\w+;)/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,
"&quot;")},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,d=this._escapedAttributes;if(!b.silent&&this.validate&&!this._performValidation(a,b))return false;if("id"in a)this.id=a.id;for(var g in a){var i=a[g];if(!f.isEqual(c[g],i)){c[g]=i;delete d[g];if(!b.silent){this._changed=true;this.trigger("change:"+g,this,i,b)}}}!b.silent&&this._changed&&this.change(b);return this},unset:function(a,b){b||(b={});var c={};c[a]=void 0;if(!b.silent&&this.validate&&
!this._performValidation(c,b))return false;delete this.attributes[a];delete this._escapedAttributes[a];if(!b.silent){this._changed=true;this.trigger("change:"+a,this,void 0,b);this.change(b)}return this},clear:function(a){a||(a={});var b=this.attributes,c={};for(attr in b)c[attr]=void 0;if(!a.silent&&this.validate&&!this._performValidation(c,a))return false;this.attributes={};this._escapedAttributes={};if(!a.silent){this._changed=true;for(attr in b)this.trigger("change:"+attr,this,void 0,a);this.change(a)}return this},
fetch:function(a){a||(a={});var b=this,c=j(a.error,b,a);(this.sync||e.sync)("read",this,function(d){if(!b.set(b.parse(d),a))return false;a.success&&a.success(b,d)},c);return this},save:function(a,b){b||(b={});if(a&&!this.set(a,b))return false;var c=this,d=j(b.error,c,b),g=this.isNew()?"create":"update";(this.sync||e.sync)(g,this,function(i){if(!c.set(c.parse(i),b))return false;b.success&&b.success(c,i)},d);return this},destroy:function(a){a||(a={});var b=this,c=j(a.error,b,a);(this.sync||e.sync)("delete",
this,function(d){b.collection&&b.collection.remove(b);a.success&&a.success(b,d)},c);return this},url:function(){var a=k(this.collection);if(this.isNew())return a;return a+(a.charAt(a.length-1)=="/"?"":"/")+this.id},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return!this.id},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=false},hasChanged:function(a){if(a)return this._previousAttributes[a]!=
this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=false,d;for(d in a)if(!f.isEqual(b[d],a[d])){c=c||{};c[d]=a[d]}return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);if(c){b.error?b.error(this,c):this.trigger("error",this,c,b);return false}return true}});
e.Collection=function(a,b){b||(b={});if(b.comparator){this.comparator=b.comparator;delete b.comparator}this._boundOnModelEvent=f.bind(this._onModelEvent,this);this._reset();a&&this.refresh(a,{silent:true});this.initialize(a,b)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c<d;c++)this._add(a[c],b);else this._add(a,b);return this},remove:function(a,
b){if(f.isArray(a))for(var c=0,d=a.length;c<d;c++)this._remove(a[c],b);else this._remove(a,b);return this},get:function(a){if(a==null)return null;return this._byId[a.id!=null?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");this.models=this.sortBy(this.comparator);a.silent||this.trigger("refresh",this,a);return this},pluck:function(a){return f.map(this.models,
function(b){return b.get(a)})},refresh:function(a,b){a||(a=[]);b||(b={});this._reset();this.add(a,{silent:true});b.silent||this.trigger("refresh",this,b);return this},fetch:function(a){a||(a={});var b=this,c=j(a.error,b,a);(this.sync||e.sync)("read",this,function(d){b.refresh(b.parse(d));a.success&&a.success(b,d)},c);return this},create:function(a,b){var c=this;b||(b={});if(a instanceof e.Model)a.collection=c;else a=new this.model(a,{collection:c});return a.save(null,{success:function(d,g){c.add(d);
b.success&&b.success(d,g)},error:b.error})},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_add:function(a,b){b||(b={});a instanceof e.Model||(a=new this.model(a,{collection:this}));var c=this.getByCid(a);if(c)throw Error(["Can't add the same model to a set twice",c.id]);this._byId[a.id]=a;this._byCid[a.cid]=a;a.collection=this;this.models.splice(this.comparator?this.sortedIndex(a,this.comparator):
this.length,0,a);a.bind("all",this._boundOnModelEvent);this.length++;b.silent||a.trigger("add",a,this,b);return a},_remove:function(a,b){b||(b={});a=this.getByCid(a)||this.get(a);if(!a)return null;delete this._byId[a.id];delete this._byCid[a.cid];delete a.collection;this.models.splice(this.indexOf(a),1);this.length--;b.silent||a.trigger("remove",a,this,b);a.unbind("all",this._boundOnModelEvent);return a},_onModelEvent:function(a,b){if(a==="change:id"){delete this._byId[b.previous("id")];this._byId[b.id]=
b}this.trigger.apply(this,arguments)}});f.each(["forEach","each","map","reduce","reduceRight","find","detect","filter","select","reject","every","all","some","any","include","invoke","max","min","sortBy","sortedIndex","toArray","size","first","rest","last","without","indexOf","lastIndexOf","isEmpty"],function(a){e.Collection.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});e.Controller=function(a){a||(a={});if(a.routes)this.routes=a.routes;this._bindRoutes();
this.initialize(a)};var o=/:([\w\d]+)/g,p=/\*([\w\d]+)/g;f.extend(e.Controller.prototype,e.Events,{initialize:function(){},route:function(a,b,c){e.history||(e.history=new e.History);f.isRegExp(a)||(a=this._routeToRegExp(a));e.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d))},this))},saveLocation:function(a){e.history.saveLocation(a)},_bindRoutes:function(){if(this.routes)for(var a in this.routes){var b=this.routes[a];
this.route(a,b,this[b])}},_routeToRegExp:function(a){a=a.replace(o,"([^/]*)").replace(p,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});e.History=function(){this.handlers=[];this.fragment=this.getFragment();f.bindAll(this,"checkUrl")};var l=/^#*/;f.extend(e.History.prototype,{interval:50,getFragment:function(a){return(a||window.location).hash.replace(l,"")},start:function(){var a=document.documentMode;if(a=h.browser.msie&&(!a||a<=7))this.iframe=h('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow;
"onhashchange"in window&&!a?h(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);return this.loadUrl()},route:function(a,b){this.handlers.push({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();if(a==this.fragment&&this.iframe)a=this.getFragment(this.iframe.location);if(a==this.fragment||a==decodeURIComponent(this.fragment))return false;if(this.iframe)window.location.hash=this.iframe.location.hash=a;this.loadUrl()},loadUrl:function(){var a=this.fragment=
this.getFragment();return f.any(this.handlers,function(b){if(b.route.test(a)){b.callback(a);return true}})},saveLocation:function(a){a=(a||"").replace(l,"");if(this.fragment!=a){window.location.hash=this.fragment=a;if(this.iframe&&a!=this.getFragment(this.iframe.location)){this.iframe.document.open().close();this.iframe.location.hash=a}}}});e.View=function(a){this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize(a)};var q=/^(\w+)\s*(.*)$/;f.extend(e.View.prototype,e.Events,
{tagName:"div",$:function(a){return h(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){h(this.el).remove();return this},make:function(a,b,c){a=document.createElement(a);b&&h(a).attr(b);c&&h(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events)){h(this.el).unbind();for(var b in a){var c=a[b],d=b.match(q),g=d[1];d=d[2];c=f.bind(this[c],this);d===""?h(this.el).bind(g,c):h(this.el).delegate(d,g,c)}}},_configure:function(a){if(this.options)a=f.extend({},
this.options,a);if(a.model)this.model=a.model;if(a.collection)this.collection=a.collection;if(a.el)this.el=a.el;if(a.id)this.id=a.id;if(a.className)this.className=a.className;if(a.tagName)this.tagName=a.tagName;this.options=a},_ensureElement:function(){if(!this.el){var a={};if(this.id)a.id=this.id;if(this.className)a["class"]=this.className;this.el=this.make(this.tagName,a)}}});var m=function(a,b){var c=r(this,a,b);c.extend=m;return c};e.Model.extend=e.Collection.extend=e.Controller.extend=e.View.extend=
m;var s={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};e.sync=function(a,b,c,d){var g=s[a];a=a==="create"||a==="update"?JSON.stringify(b.toJSON()):null;b={url:k(b),type:g,contentType:"application/json",data:a,dataType:"json",processData:false,success:c,error:d};if(e.emulateJSON){b.contentType="application/x-www-form-urlencoded";b.processData=true;b.data=a?{model:a}:{}}if(e.emulateHTTP)if(g==="PUT"||g==="DELETE"){if(e.emulateJSON)b.data._method=g;b.type="POST";b.beforeSend=function(i){i.setRequestHeader("X-HTTP-Method-Override",
g)}}h.ajax(b)};var n=function(){},r=function(a,b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};n.prototype=a.prototype;d.prototype=new n;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},k=function(a){if(!(a&&a.url))throw Error("A 'url' property or function must be specified");return f.isFunction(a.url)?a.url():a.url},j=function(a,b,c){return function(d){a?a(b,d):b.trigger("error",b,d,c)}}})();

1098
www/js/backbone.js Normal file

File diff suppressed because it is too large Load Diff

419
www/js/showdown-min.js vendored Normal file
View File

@ -0,0 +1,419 @@
/*
A A L Source code at:
T C A <http://www.attacklab.net/>
T K B
*/
var Showdown={};
Showdown.converter=function(){
var _1;
var _2;
var _3;
var _4=0;
this.makeHtml=function(_5){
_1=new Array();
_2=new Array();
_3=new Array();
_5=_5.replace(/~/g,"~T");
_5=_5.replace(/\$/g,"~D");
_5=_5.replace(/\r\n/g,"\n");
_5=_5.replace(/\r/g,"\n");
_5="\n\n"+_5+"\n\n";
_5=_6(_5);
_5=_5.replace(/^[ \t]+$/mg,"");
_5=_7(_5);
_5=_8(_5);
_5=_9(_5);
_5=_a(_5);
_5=_5.replace(/~D/g,"$$");
_5=_5.replace(/~T/g,"~");
return _5;
};
var _8=function(_b){
var _b=_b.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|\Z)/gm,function(_c,m1,m2,m3,m4){
m1=m1.toLowerCase();
_1[m1]=_11(m2);
if(m3){
return m3+m4;
}else{
if(m4){
_2[m1]=m4.replace(/"/g,"&quot;");
}
}
return "";
});
return _b;
};
var _7=function(_12){
_12=_12.replace(/\n/g,"\n\n");
var _13="p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del";
var _14="p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math";
_12=_12.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm,_15);
_12=_12.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm,_15);
_12=_12.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,_15);
_12=_12.replace(/(\n\n[ ]{0,3}<!(--[^\r]*?--\s*)+>[ \t]*(?=\n{2,}))/g,_15);
_12=_12.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,_15);
_12=_12.replace(/\n\n/g,"\n");
return _12;
};
var _15=function(_16,m1){
var _18=m1;
_18=_18.replace(/\n\n/g,"\n");
_18=_18.replace(/^\n/,"");
_18=_18.replace(/\n+$/g,"");
_18="\n\n~K"+(_3.push(_18)-1)+"K\n\n";
return _18;
};
var _9=function(_19){
_19=_1a(_19);
var key=_1c("<hr />");
_19=_19.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,key);
_19=_19.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm,key);
_19=_19.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm,key);
_19=_1d(_19);
_19=_1e(_19);
_19=_1f(_19);
_19=_7(_19);
_19=_20(_19);
return _19;
};
var _21=function(_22){
_22=_23(_22);
_22=_24(_22);
_22=_25(_22);
_22=_26(_22);
_22=_27(_22);
_22=_28(_22);
_22=_11(_22);
_22=_29(_22);
_22=_22.replace(/ +\n/g," <br />\n");
return _22;
};
var _24=function(_2a){
var _2b=/(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|<!(--.*?--\s*)+>)/gi;
_2a=_2a.replace(_2b,function(_2c){
var tag=_2c.replace(/(.)<\/?code>(?=.)/g,"$1`");
tag=_2e(tag,"\\`*_");
return tag;
});
return _2a;
};
var _27=function(_2f){
_2f=_2f.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,_30);
_2f=_2f.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()<?(.*?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,_30);
_2f=_2f.replace(/(\[([^\[\]]+)\])()()()()()/g,_30);
return _2f;
};
var _30=function(_31,m1,m2,m3,m4,m5,m6,m7){
if(m7==undefined){
m7="";
}
var _39=m1;
var _3a=m2;
var _3b=m3.toLowerCase();
var url=m4;
var _3d=m7;
if(url==""){
if(_3b==""){
_3b=_3a.toLowerCase().replace(/ ?\n/g," ");
}
url="#"+_3b;
if(_1[_3b]!=undefined){
url=_1[_3b];
if(_2[_3b]!=undefined){
_3d=_2[_3b];
}
}else{
if(_39.search(/\(\s*\)$/m)>-1){
url="";
}else{
return _39;
}
}
}
url=_2e(url,"*_");
var _3e="<a href=\""+url+"\"";
if(_3d!=""){
_3d=_3d.replace(/"/g,"&quot;");
_3d=_2e(_3d,"*_");
_3e+=" title=\""+_3d+"\"";
}
_3e+=">"+_3a+"</a>";
return _3e;
};
var _26=function(_3f){
_3f=_3f.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,_40);
_3f=_3f.replace(/(!\[(.*?)\]\s?\([ \t]*()<?(\S+?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,_40);
return _3f;
};
var _40=function(_41,m1,m2,m3,m4,m5,m6,m7){
var _49=m1;
var _4a=m2;
var _4b=m3.toLowerCase();
var url=m4;
var _4d=m7;
if(!_4d){
_4d="";
}
if(url==""){
if(_4b==""){
_4b=_4a.toLowerCase().replace(/ ?\n/g," ");
}
url="#"+_4b;
if(_1[_4b]!=undefined){
url=_1[_4b];
if(_2[_4b]!=undefined){
_4d=_2[_4b];
}
}else{
return _49;
}
}
_4a=_4a.replace(/"/g,"&quot;");
url=_2e(url,"*_");
var _4e="<img src=\""+url+"\" alt=\""+_4a+"\"";
_4d=_4d.replace(/"/g,"&quot;");
_4d=_2e(_4d,"*_");
_4e+=" title=\""+_4d+"\"";
_4e+=" />";
return _4e;
};
var _1a=function(_4f){
_4f=_4f.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,function(_50,m1){
return _1c("<h1>"+_21(m1)+"</h1>");
});
_4f=_4f.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,function(_52,m1){
return _1c("<h2>"+_21(m1)+"</h2>");
});
_4f=_4f.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,function(_54,m1,m2){
var _57=m1.length;
return _1c("<h"+_57+">"+_21(m2)+"</h"+_57+">");
});
return _4f;
};
var _58;
var _1d=function(_59){
_59+="~0";
var _5a=/^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;
if(_4){
_59=_59.replace(_5a,function(_5b,m1,m2){
var _5e=m1;
var _5f=(m2.search(/[*+-]/g)>-1)?"ul":"ol";
_5e=_5e.replace(/\n{2,}/g,"\n\n\n");
var _60=_58(_5e);
_60=_60.replace(/\s+$/,"");
_60="<"+_5f+">"+_60+"</"+_5f+">\n";
return _60;
});
}else{
_5a=/(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g;
_59=_59.replace(_5a,function(_61,m1,m2,m3){
var _65=m1;
var _66=m2;
var _67=(m3.search(/[*+-]/g)>-1)?"ul":"ol";
var _66=_66.replace(/\n{2,}/g,"\n\n\n");
var _68=_58(_66);
_68=_65+"<"+_67+">\n"+_68+"</"+_67+">\n";
return _68;
});
}
_59=_59.replace(/~0/,"");
return _59;
};
_58=function(_69){
_4++;
_69=_69.replace(/\n{2,}$/,"\n");
_69+="~0";
_69=_69.replace(/(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm,function(_6a,m1,m2,m3,m4){
var _6f=m4;
var _70=m1;
var _71=m2;
if(_70||(_6f.search(/\n{2,}/)>-1)){
_6f=_9(_72(_6f));
}else{
_6f=_1d(_72(_6f));
_6f=_6f.replace(/\n$/,"");
_6f=_21(_6f);
}
return "<li>"+_6f+"</li>\n";
});
_69=_69.replace(/~0/g,"");
_4--;
return _69;
};
var _1e=function(_73){
_73+="~0";
_73=_73.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,function(_74,m1,m2){
var _77=m1;
var _78=m2;
_77=_79(_72(_77));
_77=_6(_77);
_77=_77.replace(/^\n+/g,"");
_77=_77.replace(/\n+$/g,"");
_77="<pre><code>"+_77+"\n</code></pre>";
return _1c(_77)+_78;
});
_73=_73.replace(/~0/,"");
return _73;
};
var _1c=function(_7a){
_7a=_7a.replace(/(^\n+|\n+$)/g,"");
return "\n\n~K"+(_3.push(_7a)-1)+"K\n\n";
};
var _23=function(_7b){
_7b=_7b.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,function(_7c,m1,m2,m3,m4){
var c=m3;
c=c.replace(/^([ \t]*)/g,"");
c=c.replace(/[ \t]*$/g,"");
c=_79(c);
return m1+"<code>"+c+"</code>";
});
return _7b;
};
var _79=function(_82){
_82=_82.replace(/&/g,"&amp;");
_82=_82.replace(/</g,"&lt;");
_82=_82.replace(/>/g,"&gt;");
_82=_2e(_82,"*_{}[]\\",false);
return _82;
};
var _29=function(_83){
_83=_83.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g,"<strong>$2</strong>");
_83=_83.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g,"<em>$2</em>");
return _83;
};
var _1f=function(_84){
_84=_84.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,function(_85,m1){
var bq=m1;
bq=bq.replace(/^[ \t]*>[ \t]?/gm,"~0");
bq=bq.replace(/~0/g,"");
bq=bq.replace(/^[ \t]+$/gm,"");
bq=_9(bq);
bq=bq.replace(/(^|\n)/g,"$1 ");
bq=bq.replace(/(\s*<pre>[^\r]+?<\/pre>)/gm,function(_88,m1){
var pre=m1;
pre=pre.replace(/^ /mg,"~0");
pre=pre.replace(/~0/g,"");
return pre;
});
return _1c("<blockquote>\n"+bq+"\n</blockquote>");
});
return _84;
};
var _20=function(_8b){
_8b=_8b.replace(/^\n+/g,"");
_8b=_8b.replace(/\n+$/g,"");
var _8c=_8b.split(/\n{2,}/g);
var _8d=new Array();
var end=_8c.length;
for(var i=0;i<end;i++){
var str=_8c[i];
if(str.search(/~K(\d+)K/g)>=0){
_8d.push(str);
}else{
if(str.search(/\S/)>=0){
str=_21(str);
str=str.replace(/^([ \t]*)/g,"<p>");
str+="</p>";
_8d.push(str);
}
}
}
end=_8d.length;
for(var i=0;i<end;i++){
while(_8d[i].search(/~K(\d+)K/)>=0){
var _91=_3[RegExp.$1];
_91=_91.replace(/\$/g,"$$$$");
_8d[i]=_8d[i].replace(/~K\d+K/,_91);
}
}
return _8d.join("\n\n");
};
var _11=function(_92){
_92=_92.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g,"&amp;");
_92=_92.replace(/<(?![a-z\/?\$!])/gi,"&lt;");
return _92;
};
var _25=function(_93){
_93=_93.replace(/\\(\\)/g,_94);
_93=_93.replace(/\\([`*_{}\[\]()>#+-.!])/g,_94);
return _93;
};
var _28=function(_95){
_95=_95.replace(/<((https?|ftp|dict):[^'">\s]+)>/gi,"<a href=\"$1\">$1</a>");
_95=_95.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,function(_96,m1){
return _98(_a(m1));
});
return _95;
};
var _98=function(_99){
function char2hex(ch){
var _9b="0123456789ABCDEF";
var dec=ch.charCodeAt(0);
return (_9b.charAt(dec>>4)+_9b.charAt(dec&15));
}
var _9d=[function(ch){
return "&#"+ch.charCodeAt(0)+";";
},function(ch){
return "&#x"+char2hex(ch)+";";
},function(ch){
return ch;
}];
_99="mailto:"+_99;
_99=_99.replace(/./g,function(ch){
if(ch=="@"){
ch=_9d[Math.floor(Math.random()*2)](ch);
}else{
if(ch!=":"){
var r=Math.random();
ch=(r>0.9?_9d[2](ch):r>0.45?_9d[1](ch):_9d[0](ch));
}
}
return ch;
});
_99="<a href=\""+_99+"\">"+_99+"</a>";
_99=_99.replace(/">.+:/g,"\">");
return _99;
};
var _a=function(_a3){
_a3=_a3.replace(/~E(\d+)E/g,function(_a4,m1){
var _a6=parseInt(m1);
return String.fromCharCode(_a6);
});
return _a3;
};
var _72=function(_a7){
_a7=_a7.replace(/^(\t|[ ]{1,4})/gm,"~0");
_a7=_a7.replace(/~0/g,"");
return _a7;
};
var _6=function(_a8){
_a8=_a8.replace(/\t(?=\t)/g," ");
_a8=_a8.replace(/\t/g,"~A~B");
_a8=_a8.replace(/~B(.+?)~A/g,function(_a9,m1,m2){
var _ac=m1;
var _ad=4-_ac.length%4;
for(var i=0;i<_ad;i++){
_ac+=" ";
}
return _ac;
});
_a8=_a8.replace(/~A/g," ");
_a8=_a8.replace(/~B/g,"");
return _a8;
};
var _2e=function(_af,_b0,_b1){
var _b2="(["+_b0.replace(/([\[\]\\])/g,"\\$1")+"])";
if(_b1){
_b2="\\\\"+_b2;
}
var _b3=new RegExp(_b2,"g");
_af=_af.replace(_b3,_94);
return _af;
};
var _94=function(_b4,m1){
var _b6=m1.charCodeAt(0);
return "~E"+_b6+"E";
};
};
if(typeof exports!='undefined')exports.Showdown=Showdown;

View File

@ -61,13 +61,6 @@
//
// **************************************************
// GitHub Flavored Markdown modifications by Tekkub
// http://github.github.com/github-flavored-markdown/
//
// Modifications are tagged with "GFM"
// **************************************************
//
// Showdown namespace
//
@ -154,66 +147,10 @@ this.makeHtml = function(text) {
// attacklab: Restore tildes
text = text.replace(/~T/g,"~");
// ** GFM ** Auto-link URLs and emails
text = text.replace(/https?\:\/\/[^"\s\<\>]*[^.,;'">\:\s\<\>\)\]\!]/g, function(wholeMatch,matchIndex){
var left = text.slice(0, matchIndex), right = text.slice(matchIndex)
if (left.match(/<[^>]+$/) && right.match(/^[^>]*>/)) {return wholeMatch}
return "<a href='" + wholeMatch + "'>" + wholeMatch + "</a>";
});
text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/ig, function(wholeMatch){return "<a href='mailto:" + wholeMatch + "'>" + wholeMatch + "</a>";});
// ** GFM ** Auto-link sha1 if GitHub.nameWithOwner is defined
text = text.replace(/[a-f0-9]{40}/ig, function(wholeMatch,matchIndex){
if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;}
var left = text.slice(0, matchIndex), right = text.slice(matchIndex)
if (left.match(/@$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;}
return "<a href='http://github.com/" + GitHub.nameWithOwner + "/commit/" + wholeMatch + "'>" + wholeMatch.substring(0,7) + "</a>";
});
// ** GFM ** Auto-link user@sha1 if GitHub.nameWithOwner is defined
text = text.replace(/([a-z0-9_\-+=.]+)@([a-f0-9]{40})/ig, function(wholeMatch,username,sha,matchIndex){
if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;}
GitHub.repoName = GitHub.repoName || _GetRepoName()
var left = text.slice(0, matchIndex), right = text.slice(matchIndex)
if (left.match(/\/$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;}
return "<a href='http://github.com/" + username + "/" + GitHub.repoName + "/commit/" + sha + "'>" + username + "@" + sha.substring(0,7) + "</a>";
});
// ** GFM ** Auto-link user/repo@sha1
text = text.replace(/([a-z0-9_\-+=.]+\/[a-z0-9_\-+=.]+)@([a-f0-9]{40})/ig, function(wholeMatch,repo,sha){
return "<a href='http://github.com/" + repo + "/commit/" + sha + "'>" + repo + "@" + sha.substring(0,7) + "</a>";
});
// ** GFM ** Auto-link #issue if GitHub.nameWithOwner is defined
text = text.replace(/#([0-9]+)/ig, function(wholeMatch,issue,matchIndex){
if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;}
var left = text.slice(0, matchIndex), right = text.slice(matchIndex)
if (left == "" || left.match(/[a-z0-9_\-+=.]$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;}
return "<a href='http://github.com/" + GitHub.nameWithOwner + "/issues/#issue/" + issue + "'>" + wholeMatch + "</a>";
});
// ** GFM ** Auto-link user#issue if GitHub.nameWithOwner is defined
text = text.replace(/([a-z0-9_\-+=.]+)#([0-9]+)/ig, function(wholeMatch,username,issue,matchIndex){
if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;}
GitHub.repoName = GitHub.repoName || _GetRepoName()
var left = text.slice(0, matchIndex), right = text.slice(matchIndex)
if (left.match(/\/$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;}
return "<a href='http://github.com/" + username + "/" + GitHub.repoName + "/issues/#issue/" + issue + "'>" + wholeMatch + "</a>";
});
// ** GFM ** Auto-link user/repo#issue
text = text.replace(/([a-z0-9_\-+=.]+\/[a-z0-9_\-+=.]+)#([0-9]+)/ig, function(wholeMatch,repo,issue){
return "<a href='http://github.com/" + repo + "/issues/#issue/" + issue + "'>" + wholeMatch + "</a>";
});
return text;
}
var _GetRepoName = function() {
return GitHub.nameWithOwner.match(/^.+\/(.+)$/)[1]
}
var _StripLinkDefinitions = function(text) {
//
// Strips link definitions from text, stores the URLs and titles in
@ -724,10 +661,10 @@ var _DoHeaders = function(text) {
// --------
//
text = text.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,
function(wholeMatch,m1){return hashBlock("<h1>" + _RunSpanGamut(m1) + "</h1>");});
function(wholeMatch,m1){return hashBlock('<h1 id="' + headerId(m1) + '">' + _RunSpanGamut(m1) + "</h1>");});
text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,
function(matchFound,m1){return hashBlock("<h2>" + _RunSpanGamut(m1) + "</h2>");});
function(matchFound,m1){return hashBlock('<h2 id="' + headerId(m1) + '">' + _RunSpanGamut(m1) + "</h2>");});
// atx-style headers:
// # Header 1
@ -751,9 +688,12 @@ var _DoHeaders = function(text) {
text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,
function(wholeMatch,m1,m2) {
var h_level = m1.length;
return hashBlock("<h" + h_level + ">" + _RunSpanGamut(m2) + "</h" + h_level + ">");
return hashBlock("<h" + h_level + ' id="' + headerId(m2) + '">' + _RunSpanGamut(m2) + "</h" + h_level + ">");
});
function headerId(m) {
return m.replace(/[^\w]/g, '').toLowerCase();
}
return text;
}
@ -1044,7 +984,6 @@ var _DoItalicsAndBold = function(text) {
text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g,
"<strong>$2</strong>");
text = text.replace(/(\w)_(\w)/g, "$1~E95E$2") // ** GFM ** "~E95E" == escaped "_"
text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g,
"<em>$2</em>");
@ -1126,7 +1065,6 @@ var _FormParagraphs = function(text) {
}
else if (str.search(/\S/) >= 0) {
str = _RunSpanGamut(str);
str = str.replace(/\n/g,"<br />"); // ** GFM **
str = str.replace(/^([ \t]*)/g,"<p>");
str += "</p>"
grafsOut.push(str);
@ -1359,3 +1297,6 @@ var escapeCharacters_callback = function(wholeMatch,m1) {
}
} // end of Showdown.converter
// export
if (typeof exports != 'undefined') exports.Showdown = Showdown;

131
www/js/test.js Normal file
View File

@ -0,0 +1,131 @@
var Test = {};
$(document).ready(function() {
Test.U = Backbone.Model.extend({
url: '/ts_api/users',
initialize: function() {
this.timelines = {};
}
});
Test.E = Backbone.Model.extend({
url: '/ts_api/entries/jdbernard/work'
});
Test.UView = Backbone.View.extend({
el: $("#user"),
model: Test.U,
initialize: function() {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.model.view = this;
},
render: function() {
this.$('.fullname').text(this.model.get('name'));
this.$('.username').text(this.model.get('id'));
}
});
Test.EList = Backbone.Collection.extend({
model: Test.E,
url: '/ts_api/entries/jdbernard/work',
initalize: function(models, options) {
this.user = options.user;
},
comparator: function(entry) {
return entry.get('timestamp');
}
});
Test.EView = Backbone.View.extend({
model: Test.E,
initialize: function() {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.model.view = this;
},
render: function() {
$(this.el).html("<span class='entry-id'>" + this.model.get('id') + "<span class='mark'>" + this.model.get('mark') + "</span>");
return this;
}
});
Test.EListView = Backbone.View.extend({
el: $("#entry-list"),
initialize: function() {
_.bindAll(this, 'render', 'refresh', 'addOne');
this.collection.bind('add', this.addOne);
this.collection.bind('refresh', this.refresh);
},
addOne: function(entry) {
var view = new Test.EView({model: entry});
$(this.el).append(view.render().el);
},
refresh: function() {
$(this.el).empty();
var thisRef = this;
this.collection.each(function(entry) {thisRef.addOne(entry)});
}
});
Test.currentUser = new Test.U;
Test.userView = new Test.UView({model: Test.currentUser});
// wire the login dialog using jQuery UI
$("#login-dialog").dialog({
autoOpen: false,
height: 400,
width: 400,
modal: true,
buttons: { Login: function(){login()} }
});
$('#login-dialog').dialog('open');
});
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) {
$("#login-dialog").dialog('close');
Test.currentUser.set({id: name.val()}, {silent: true});
Test.currentUser.fetch();
Test.currentEntryList = Test.currentUser.timelines['work'] = new Test.EList([], {user: Test.currentUser});
new Test.EListView({collection: Test.currentEntryList});
Test.currentEntryList.fetch();
}});
}

View File

@ -1,58 +1,575 @@
var user = [];
var timelines = [];
var activeTimeline = [];
var entries = [];
var newEntryBar;
var moreEntriesbar;
var currentEntryOffset = 0;
var loadLength = 20;
var loginTop;
// TimeStamper namespace
var TS = {};
/* Setup after the document is ready for manipulation. */
$(document).ready(function(){
// looked up once and remembered
newEntryBar = $("#new-entry");
moreEntriesBar = $("#more-entries");
// ======== DEFINE MODELS ========//
/* Entry model.
* Attributes
* - id
* - mark
* - notes
* - start
*/
TS.EntryModel = Backbone.Model.extend({
// wire the login dialog using jQuery UI
$("#login-dialog").dialog({
autoOpen: false,
height: 300,
width: 300,
modal: true,
buttons: {
"Sign Up": function(){signup()},
Login: function(){login()} }
});
// TODO: add a hook to AJAX requests to check for 401 unauth
// and re-display the login dialog.
/* Timeline model.
* Attributes:
* - id
* - description
* - created
*/
TS.TimelineModel = Backbone.Model.extend({
// 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"); },
/* User model.
* Attributes:
* - username
* - fullname
* - email
* - join_date
*/
TS.UserModel = Backbone.Model.extend({
url: function() { return '/ts_api/users/' + this.get('id'); },
success: function(data, textStatus, jqXHR) {
// load the user information
loadUser(data.user.username); }});
initialize: function(attrs, options) {
_.bind(this, 'url');
}
});
})
TS.EntryList = Backbone.Collection.extend({
model: TS.EntryModel,
/* 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");
comparator: function(entry) { return entry.get('timestamp'); },
initialize: function(model, options) {
if (options.timeline == undefined) {
throw "Cannot create an EntryList without a TimelineModel reference."
} else { this.timeline = options.timeline; }
_.bindAll(this, "url");
},
url: function() {
return "/ts_api/entries/" + this.timeline.get('user_id') + "/" + this.timeline.get('id');
}
});
TS.TimelineList = Backbone.Collection.extend({
model: TS.TimelineModel,
initialize: function(models, options) {
if (options.user == undefined) {
throw "Cannot create a TimelineList without a UserModel reference.";
} else { this.user = options.user; }
_.bindAll(this, 'url');
},
comparator: function(timeline) {
return timeline.get('id');
},
url: function() {
return "/ts_api/timelines/" + this.user.get('id');
}
});
// ======== DEFINE VIEWS ========//
/* Entry view
*/
TS.EntryView = Backbone.View.extend({
model: TS.EntryModel,
className: 'entry',
notesCache: false,
events: {
"click img.expand-entry" : "showNotes",
"click img.collapse-entry" : "hideNotes",
"dblclick div.mark" : "editMark",
"dblclick div.timestamp" : "editTimestamp",
"dblclick div.notes" : "editNotes",
"keypress .mark-input" : "updateOnEnter",
"keypress .timestamp-input" : "updateOnEnter",
"keypress .notes-input" : "updateOnCtrlEnter",
"blur .mark-input" : "close",
"blur .timestamp-input" : "close",
"blur .notes-input" : "close"
},
initialize: function(options) {
_.bindAll(this, 'render', 'close', 'editTImestamp', 'editMark',
'update', 'updateOnEnter', 'updateOnCtrlEnter', 'getViewModel',
'renderNotes', 'showNotes', 'hideNotes');
this.markdownConverter = options.markdownConverter;
this.model.bind('change', this.update);
this.model.view = this;
this.nextModel = options.nextModel;
},
/**
* Refresh the display based on the model replacing the existing
* HTML content. Add new `blur` listeners to the input fields.
*/
render: function() {
// render the HTML
$(this.el).html(ich.entryTemplate(this.getViewModel()));
// invalidate the notes display cache
this.notesCache = false;
return this;
},
/**
* Refresh the display based on the model using the existing DOM
* elements.
*/
update: function() {
var data = this.getViewModel();
this.$('.mark span').text(data.mark);
this.$('.mark-input').val(data.mark);
this.$('.timestamp').text(data.start);
this.$('.timestamp-input').val(data.timestamp);
this.$('.duration').text(data.duration);
this.$('.notes-text').html(this.renderNotes(data.notes));
this.$('.notes-input').val(data.notes);
return this;
},
renderNotes: function(source) {
if (!this.notesCache) {
this.notesCache = this.markdownConverter.makeHtml(source);
}
return this.notesCache
},
editMark: function() {
$(this.el).addClass('edit-mark');
this.$('.mark-input').focus();
return this;
},
editTimestamp: function() {
$(this.el).addClass('edit-timestamp');
this.$('timestamp-input').focus();
return this;
},
editNotes: function() {
// invalidate notes HTML cache
this.notesCache = false;
// show notes textarea, hide display
$(this.el).addClass('edit-notes');
// focus input
this.$('.notes-input').focus();
return this;
},
/**
* Translate the model data into a form suitable to be displayed.
* @return a map including display-able `start` and `duration` values.
*/
getViewModel: function() {
var data = this.model.toJSON();
// create start and duration values
var tsDate = new Date(data.timestamp);
data.start = this.formatStart(tsDate);
data.duration = this.formatDuration(this.model, this.nextModel);
data.notes = data.notes ? data.notes : '*No notes for this entry.*';
return data;
},
/** Close editable fields. */
close: function() {
$(this.el).removeClass('edit-mark edit-timestamp edit-notes');
},
/** Persist changes in input fields. */
save: function() {
this.model.save({
mark: this.$('.mark-input').val(),
timestamp: this.$('.timestamp-input').val(),
notes: this.$('.notes-input').val()});
},
/** Event handler for keypresses on entry input fields. */
updateOnEnter: function(e) {
if(e.keyCode == 13) { this.save(); this.close(); }
},
updateOnCtrlEnter: function(e) {
if (e.keyCode == 10) { this.save(); this.close(); }
},
/**
* Get the display-able start time from the entry timestamp.
* @param startDate a Date object, the entry timestamp.
* @return display-able start time in HH:MM format.
*/
formatStart: function(startDate) {
/* Code is written for both 24hr and 12hr formats. I still need to
* create a mechanism for selecting between them. For now, use the
* 12hr format. */
// 24 hour
// return startDate.getHours() + ":" + startDate.getMinutes();
// 12 hour
var hour = startDate.getHours() % 12;
return (hour == 0 ? 12 : hour) + ":" + startDate.getMinutes() +
" " + (startDate.getHours() > 11 ? "pm" : "am");
},
/**
* Get the duration of the entry based on this entry's timestamp and
* and the next entry's timestamp in a display-able form. If nextModel
* is `null` or `undefined` it is assumed that `model` is the most
* recent model and duration is calculated against the current time.
*
* @param model EntryModel representing this entry.
* @param nextModel EntryModel representing the next entry.
* @return the duration between model and nextModel, formatted for
* display: `Xd Yhr Zm`. */
formatDuration: function(model, nextModel) {
var d1 = new Date(model.get('timestamp'));
var d2, diff;
var day, hr, min;
// if no next model, assume it's an onoing task
if (nextModel) { d2 = new Date(nextModel.get('timestamp')); }
else { d2 = new Date(); }
diff= d2.getTime() - d1.getTime();
day = Math.floor(diff / 86400000); // milliseconds in a day
diff %= 86400000;
hr = Math.floor(diff / 3600000); // millis in an hour
diff %= 3600000;
min = Math.floor(diff / 60000); // millis in a minute
return (day > 0 ? day + "d " : "") +
(hr > 0 ? hr + "hr " : "") +
min + "m ";
},
showNotes: function() {
if (!this.notesCache) {
this.$('.notes-text').html(
this.renderNotes(this.model.get('notes')))
}
this.$('.notes').slideDown();
$(this.el).addClass('show-notes');
},
hideNotes: function() {
this.$('.notes').slideUp();
$(this.el).removeClass('show-notes');
}
});
TS.EntryListView = Backbone.View.extend({
el: $("#entry-list"),
events: {
"keypress #new-entry-input" : "createNewEntryOnEnter"
},
initialize: function() {
_.bindAll(this, 'addOne', 'createNewEntry', 'render', 'renderOne');
this.collection.bind('add', this.addOne);
this.collection.bind('refresh', this.render);
this.collection.view = this;
this.entryContainer = this.$("#entries")
this.markdownConverter = new Showdown.converter();
},
addOne: function(entry) {
var lastEntry = this.collection.at(this.collection.length - 2);
lastEntry.view.nextModel = entry;
lastEntry.view.update();
this.renderOne(entry, null);
},
renderOne: function(entry, nextEntry) {
if (!entry.view) { new TS.EntryView(
{model: entry, markdownConverter: this.markdownConverter}); }
entry.view.nextModel = nextEntry
this.entryContainer.prepend(entry.view.render().el);
},
createNewEntryOnEnter: function(e) {
if (e.keyCode == 13) {
// grab the mark data
var entryMark = this.$("#new-entry-input").val();
// create the mark. Immediately fetch to get server-side timestamp
this.collection.create({mark: entryMark,
notes: '',
timestamp: getUTCTimestamp()}).fetch();
// clear the input for the next entry
this.$("#new-entry-input").val("");
}
},
render: function() {
this.entryContainer.empty();
for (var i = 0, len = this.collection.length; i < len; i++) {
var entry = this.collection.at(i);
var nextEntry = (i + 1 < len ? this.collection.at(i + 1) : null);
this.renderOne(entry, nextEntry);
}
}
});
TS.TimelineListView = Backbone.View.extend({
el: $("#timeline"),
collection: TS.TimelineList,
events: {
"dblclick .timeline-id" : "editId",
"dblclick .timeline-desc" : "editDesc",
"keypress .timeline-id-input" : "updateOnEnter",
"keypress .timeline-desc-input" : "updateOnEnter",
"click .new-timeline-link" : "showNewTimelineDialog"
},
initialize: function(options) {
_.bindAll(this, 'render', 'renderOne', 'editId',
'editDesc', 'updateOnEnter');
if (options.initialTimelineId == undefined) {
throw "Can not create a TimelineListView without an initial timeline."
} else {
this.selected = this.collection.get(options.initialTimelineId);
}
this.collection.bind('add', this.renderOne);
this.collection.bind('refresh', this.render);
},
renderOne: function(timeline) {
this.$('.drop-menu-items').append(
ich.timelineLinkTemplate(timeline.toJSON()));
},
render: function() {
// render the basic template
$(this.el).html(ich.timelineTemplate(this.selected.toJSON()));
// render the selection list
_.each(this.collection.without([this.selected]), this.renderOne);
},
editId: function() {
$(this.el).addClass('edit-id');
this.$('.timeline-id-input').focus();
return this;
},
editDesc: function() {
$(this.el).addClass('edit-desc');
this.$('.timeline-desc-input').focus();
return this;
},
close: function() {
this.selected.save({
id: this.$('.timeline-id-input').val(),
description: this.$('.timeline-desc-input').val()});
$(this.el).removeClass('edit-id edit-desc');
this.render();
},
showNewTimelineDialog: function() {
TS.app.newTimelineDialog.show();
},
updateOnEnter: function(e) {
if (e.keyCode == 13) { this.close(); this.save() }
}
});
TS.UserView = Backbone.View.extend({
el: $("#user"),
model: TS.UserModel,
events: {
'dblclick .fullname': 'editFullname',
'keypress .fullname-input': 'updateOnEnter'
},
initialize: function() {
_.bindAll(this, 'render', 'close', 'editFullname', 'updateOnEnter');
this.model.bind('change', this.render);
this.model.view = this;
},
render: function() {
$(this.el).html(ich.userTemplate(this.model.toJSON()));
return this;
},
editFullname: function() {
$(this.el).addClass('edit-fullname');
this.$('.fullname-input').focus();
return this;
},
close: function() {
this.model.set({name: this.$('fullname-input').val()});
this.model.save();
$(this.el).removeClass('edit-fullname');
},
updateOnEnter: function(e) {
if (e.keyCode == 13) this.close();
}
});
TS.AppView = Backbone.View.extend({
el: $("body"),
events: {
'click #timeline .drop-menu-items a': 'selectTimeline'
},
initialize: function() {
_.bindAll(this, 'initializeViews', 'loadInitialData');
appThis = this;
// create the login dialog
this.loginDialog = new TS.LoginView
// initialize data, either from boostrapped data, or via user login
if (window.bootstrap) { this.initializeData(window.bootstrap()) }
else {
// this is async (waiting for user input)
this.loginDialog.authenticate(function() {
appThis.initializeData(appThis.loadInitialData())});
}
},
initializeData: function(data) {
// create user data
this.user = {};
this.user.model = new TS.UserModel(data.user);
this.user.view = new TS.UserView({model: this.user.model});
// create timeline models from the bootstrapped data
var tlModels = _.map(data.timelines, function(timeline) {
return new TS.TimelineModel(timeline);
});
// create the timeline list collection
this.timelines = {};
this.timelines.collection = new TS.TimelineList(
tlModels, {user: this.user.model});
this.timelines.view = new TS.TimelineListView(
{collection: this.timelines.collection,
initialTimelineId: data.initialTimelineId});
// create the new timeline dialog
this.newTimelineDialog = new TS.NewTimelineView(
{timelineCollection: this.timelines.collection});
// create entry models from the bootstrapped data
var entryModels = _.map(data.entries, function(entry) {
return new TS.EntryModel(entry);
});
// create the entry collection
this.entries = {};
this.entries.collection = new TS.EntryList(entryModels,
{timeline: this.timelines.view.selected});
this.entries.view = new TS.EntryListView(
{collection: this.entries.collection});
// render views
this.user.view.render();
this.timelines.view.render();
this.entries.view.render();
},
loadInitialData: function() {
// assume we are authenticated
var username = $("#username-input").val(); // hackish
var data = jQuery.parseJSON($.ajax({
url: '/ts_api/app/user_summary/' + username,
async: false}).responseText);
data.initialTimelineId = data.timelines[0].id;
data.entries = jQuery.parseJSON($.ajax({
url: '/ts_api/entries/' + username + '/' +
data.initialTimelineId,
async: false}).responseText);
return data;
}
});
TS.LoginView = Backbone.View.extend({
el: $("#login"),
events: {
"keypress #password-input" : "loginOnEnter",
"click #login-button a" : "doLogin"
},
initialize: function() {
_.bindAll(this, 'authenticate', 'doLogin', 'hide', 'loginOnEnter',
'show');
},
action: function() {},
authenticate: function(nextAction) {
this.action = nextAction;
this.show();
},
doLogin: function(){
var viewThis = this;
var name = this.$("#username-input");
var pwd = $("#password-input");
// call the API via AJAX
$.ajax({
@ -60,312 +577,77 @@ function login() {
processData: false,
data: JSON.stringify({username: name.val(), password: pwd.val()}),
type: "POST",
async: false,
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");
}});
}
function toggleSignUp(event) {
var signUpCB = $("#signup-checkbox");
if (signUpCB.attr("checked")) {
loginTop = $("#login-dialog").dialog("widget").offset().top;
$("#login-dialog").animate({height: 350}, 500);
$("#login-dialog").dialog("widget").animate({top: loginTop - 200}, 500);
$(".signup").slideDown("slow");
} else {
$("#login-dialog").animate({height: 180}, 500);
$("#login-dialog").dialog("widget").animate({top: loginTop}, 500);
$(".signup").slideUp("slow");
viewThis.hide();
viewThis.action();
}
}
function signUp() {
}
/* 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)
}
});
}
hide: function() { $(this.el).addClass('hidden'); },
/* 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;
show: function() {
$(this.el).removeClass('hidden');
this.$("#username-input").focus();
},
error: function(jqXHR, textStatus, error) {
alert(jqXHR.responseText);
loginOnEnter: function(e) {
if (e.keyCode == 13) { this.doLogin(); }
}
});
}
/* 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) {
TS.NewTimelineView = Backbone.View.extend({
el: $("#new-timeline"),
// for each entry
_.each(entries, function(entry) {
events: {
"click #new-timeline-create a" : "createTimeline",
"click #new-timeline-cancel a" : "hide"
},
// remove the existing top-entry designation
$(".top-entry").removeClass("top-entry");
initialize: function(options) {
_.bindAll(this, 'createTimeline', 'hide', 'show');
// create the new entry from the entry template (see ICanHas.js)
var entryElem = ich.entry(entry);
if (options.timelineCollection == undefined) {
throw "Can not create the NewTimelineView without the timeline collection."
} else {
this.timelineCollection = options.timelineCollection;
}
// 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);
createTimeline: function() {
var timelineId = this.$("#new-timeline-id").val();
var timelineDesc = this.$("#new-timeline-desc").val();
this.timelineCollection.create(
{id: timelineId, description: timelineDesc,
created: getUTCTimestamp()});
this.hide();
},
// 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.
hide: function() { $(this.el).addClass('hidden'); },
show: function() {
this.$("#new-timeline-id").val("");
this.$("#new-timeline-desc").val("");
$(this.el).removeClass('hidden');
this.$("#new-timeline-id").focus();
}
});
}
/* 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) {
TS.app = new TS.AppView;
// 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();
@ -378,3 +660,4 @@ function getUTCTimestamp() {
+ pad(d.getUTCMinutes())+':'
+ pad(d.getUTCSeconds())+'Z';
}

247
www/prototype.html Normal file
View File

@ -0,0 +1,247 @@
<!DOCTYPE html>
<html>
<head>
<title>TimeStamper - Simple Time Tracking</title>
<link href='http://fonts.googleapis.com/css?family=Arvo|Bentham|Cuprum|Cantarell|Geo|Josefin+Sans' rel='stylesheet' type='text/css'>
<link rel="stylesheet" media="screen" href="css/ts-screen.css" type="text/css"/>
<script type="text/javascript" src="js/jquery-1.5.js"></script>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
</head>
<body>
<div id="top">
<div id="timeline">
<span class="timeline-desc">Work-related activities.</span>
<input class="timeline-desc-input" type="text"/>
<div class="drop-menu">
<div class="timeline-id">(&nbsp;work&nbsp;)</div>
<input class="timeline-id-input" type="text"/>
<ul class="drop-menu-items">
<li class="timeline-link"><a href="#">jdb-labs</a></li>
<li class="timeline-link"><a href="#">personal</a></li>
<li class="timeline-link"><a href="#">vbs-suite</a></li>
</ul>
</div>
</div>
<div id="user">
<div class="fullname">Jonathan Bernard</div>
<input class='fullname-input' type='text'/>
<div class='drop-menu'>
<div class="username"> - jdbernard</div>
<ul class="drop-menu-items">
<li><a href="#">logout</a></li>
<li><a href="#">user info</a></li>
</ul>
</div>
</div>
</div>
<div id="entry-list">
<div class="day-seperator">
<h4 class='mark'>Today</h4>
<h5 class='timestamp'>start</h5>
<h5 class='duration'>duration</h5>
</div>
<div id="new-entry">
<input id="new-entry-input" class="mark-input"
placeholder="Start a new task..." type="text" />
</div>
<div id="entries">
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp: Entering tickets.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">12:32</div>
<input class="timestamp-input" type="text"/>
<div class="duration">4<span class="tick-tock">hr </span>3<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp: Helping Steve wth WR Updates.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">9:56</div>
<input class="timestamp-input" type="text"/>
<div class="duration">1<span class="tick-tock">hr </span>15<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>Email</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">9:10</div>
<input class="timestamp-input" type="text"/>
<div class="duration">47<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="day-seperator">
<h4 class='mark'>Yesterday</h4>
<h5 class='timestamp'>start</h5>
<h5 class='duration'>duration</h5>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp: Working #7801.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">3:12 pm</div>
<input class="timestamp-input" type="text"/>
<div class="duration">12<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>Lunch.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">11:47 am</div>
<input class="timestamp-input" type="text"/>
<div class="duration">3<span class="tick-tock">hr </span>25<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp: Entering tickets.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">9:20 am</div>
<input class="timestamp-input" type="text"/>
<div class="duration">2<span class="tick-tock">hr </span>27<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp: Reproducing #7796.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">9:11 am</div>
<input class="timestamp-input" type="text"/>
<div class="duration">9<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="day-seperator">
<h4 class='mark'>Monday</h4>
<h5 class='timestamp'>start</h5>
<h5 class='duration'>duration</h5>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp: Working #7733.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">16:41</div>
<input class="timestamp-input" type="text"/>
<div class="duration">1<span class="tick-tock">hr </span>8<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>Zend Training: Building Security Into Your PHP Applications</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">10:30</div>
<input class="timestamp-input" type="text"/>
<div class="duration">4<span class="tick-tock">hr </span>11<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">8:40</div>
<input class="timestamp-input" type="text"/>
<div class="duration">1<span class="tick-tock">hr </span>50<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="day-seperator">
<h4 class='mark'>Friday, April 29th</h4>
<h5 class='timestamp'>start</h5>
<h5 class='duration'>duration</h5>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>Training Steve: Databases and SQL</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">16:09</div>
<input class="timestamp-input" type="text"/>
<div class="duration">55<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>Preparing Instructional Material: Database Basics</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">15:12</div>
<input class="timestamp-input" type="text"/>
<div class="duration">57<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp: Working #7729.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">15:09</div>
<input class="timestamp-input" type="text"/>
<div class="duration">3<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">14:44</div>
<input class="timestamp-input" type="text"/>
<div class="duration">25<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp: Working #7728.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">14:41</div>
<input class="timestamp-input" type="text"/>
<div class="duration">3<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">14:00</div>
<input class="timestamp-input" type="text"/>
<div class="duration">41<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>Lunch.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">13:05</div>
<input class="timestamp-input" type="text"/>
<div class="duration">55<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp: Working #7725.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">12:40</div>
<input class="timestamp-input" type="text"/>
<div class="duration">20<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>Zend Training: Building Security Into You PHP Applications.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">10:30</div>
<input class="timestamp-input" type="text"/>
<div class="duration">2<span class="tick-tock">hr </span>10<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>Zend Training: Preparing for security training.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">09:25</div>
<input class="timestamp-input" type="text"/>
<div class="duration">1<span class="tick-tock">hr </span>5<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
<div class="entry">
<div class="mark"><img src="img/round_delete_icon&16.png" class="delete-icon"/><span>ITHelp: Working #7700.</span><img src="img/notepad_2_icon&16.png" class="notes-icon"/></div>
<input class="mark-input" type="text"/>
<div class="timestamp">09:17</div>
<input class="timestamp-input" type="text"/>
<div class="duration">8<span class="tick-tock">m </span></div>
<div class="notes">Some notes should go here, but they should be hidden by default</div>
</div>
</div>
</div>
<div class="footer">
Copyright 2011 <a href="http://www.jdb-labs.com"><span class="logo">JDB Labs</span> LLC.</a>
</div>
</body>
</html>

38
www/test.html Normal file
View File

@ -0,0 +1,38 @@
<html>
<head>
<title>Testing Backbone.js</title>
<link rel="stylesheet" media="screen" href="/css/dot-luv/jquery-ui-1.8.10.custom.css" type="text/css"/>
<script type="text/javascript" src="/js/jquery-1.5.min.js"></script>
<script type="text/javascript" src="/js/jquery-ui-1.8.10.custom.min.js"></script>
<script type="text/javascript" src="/js/underscore-min.js"></script>
<script type="text/javascript" src="/js/ICanHaz.js"></script>
<script type="text/javascript" src="/js/backbone-min.js"></script>
<script type="text/javascript" src="/js/test.js"></script>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
</head>
<body>
<div id="user">
<div class="username">username</div>
<div class="fullname">fullname</div>
</div>
<div id="entry-list"></div>
<div id="login-dialog" title="Login">
<form>
<fieldset>
<label for="login-name">Username:</label>
<input type="text" name="login-name" id="login-name"
class="text ui-widget-content ui-corner-all"></input>
<label for="login-password">Password:</label>
<input type="password" name="login-password" id="login-password"
class="text ui-widget-content ui-corner-all"></input>
</fieldset>
</form>
<p class="validate-tips"></p>
</div>
</body>
</html>

View File

@ -3,7 +3,7 @@ ebin_dir = /home/jdbernard/projects/jdb-labs/timestamper/web-app/ebin
runmod = timestamper_dev
<server timestamper.jdb-labs-local>
<server timestamper-local>
port = 8000
listen = 127.0.0.1
docroot = /home/jdbernard/projects/jdb-labs/timestamper/web-app/www