├── .gitignore
├── resources
├── public
│ ├── favicon.ico
│ ├── mp3player.swf
│ ├── javascript
│ │ ├── bootstrap.js
│ │ ├── preferences.js
│ │ ├── ajax_service.js
│ │ ├── animation.js
│ │ ├── external
│ │ │ ├── jquery.url.js
│ │ │ ├── jquery.color.js
│ │ │ ├── shortcut.js
│ │ │ ├── AC_RunActiveContent.js
│ │ │ └── date.js
│ │ ├── input_hints.js
│ │ ├── change-password.js
│ │ ├── countdown.js
│ │ ├── library.js
│ │ ├── register.js
│ │ ├── sound_player.js
│ │ ├── recovery.js
│ │ └── index.js
│ ├── sounds
│ │ ├── alarm.mp3
│ │ ├── alarm.ogg
│ │ ├── alarm.wav
│ │ ├── ticking.mp3
│ │ ├── ticking.ogg
│ │ └── ticking.wav
│ ├── theme
│ │ ├── images
│ │ │ ├── error.gif
│ │ │ ├── button.png
│ │ │ ├── pomodoro.png
│ │ │ ├── tutorial.png
│ │ │ ├── validation.png
│ │ │ ├── preferences.png
│ │ │ └── validation_double.png
│ │ └── css
│ │ │ ├── reset.css
│ │ │ └── master.css
│ └── ga.js
├── queries.sql
└── migrations.sql
├── dev-resources
└── mytomatoes-config.edn
├── src
└── mytomatoes
│ ├── util.clj
│ ├── server.clj
│ ├── account.clj
│ ├── csv.clj
│ ├── pages
│ ├── error.clj
│ ├── login.clj
│ ├── recovery.clj
│ └── home.clj
│ ├── migrations.clj
│ ├── storage.clj
│ ├── login.clj
│ ├── system.clj
│ ├── web.clj
│ ├── layout.clj
│ ├── word_stats.clj
│ └── actions.clj
├── README.md
├── dev
└── user.clj
├── project.clj
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | pom.xml
2 | target
3 | /.lein-repl-history
4 | /.nrepl-port
5 | /debug.log
6 |
--------------------------------------------------------------------------------
/resources/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/favicon.ico
--------------------------------------------------------------------------------
/resources/public/mp3player.swf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/mp3player.swf
--------------------------------------------------------------------------------
/resources/public/javascript/bootstrap.js:
--------------------------------------------------------------------------------
1 | /*global MT */
2 |
3 | MT.initialize_preferences();
4 | MT.initialize_index();
5 |
--------------------------------------------------------------------------------
/resources/public/sounds/alarm.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/sounds/alarm.mp3
--------------------------------------------------------------------------------
/resources/public/sounds/alarm.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/sounds/alarm.ogg
--------------------------------------------------------------------------------
/resources/public/sounds/alarm.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/sounds/alarm.wav
--------------------------------------------------------------------------------
/resources/public/sounds/ticking.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/sounds/ticking.mp3
--------------------------------------------------------------------------------
/resources/public/sounds/ticking.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/sounds/ticking.ogg
--------------------------------------------------------------------------------
/resources/public/sounds/ticking.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/sounds/ticking.wav
--------------------------------------------------------------------------------
/resources/public/theme/images/error.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/theme/images/error.gif
--------------------------------------------------------------------------------
/resources/public/theme/images/button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/theme/images/button.png
--------------------------------------------------------------------------------
/resources/public/theme/images/pomodoro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/theme/images/pomodoro.png
--------------------------------------------------------------------------------
/resources/public/theme/images/tutorial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/theme/images/tutorial.png
--------------------------------------------------------------------------------
/resources/public/theme/images/validation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/theme/images/validation.png
--------------------------------------------------------------------------------
/resources/public/theme/images/preferences.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/theme/images/preferences.png
--------------------------------------------------------------------------------
/resources/public/theme/images/validation_double.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magnars/mytomatoes.clj/master/resources/public/theme/images/validation_double.png
--------------------------------------------------------------------------------
/dev-resources/mytomatoes-config.edn:
--------------------------------------------------------------------------------
1 | {:db {:subprotocol "mysql"
2 | :subname "//docker:3306/mytomatoes?characterEncoding=utf8"
3 | :user "root"
4 | :password "foobar"}
5 | :memcached "localhost:11211"
6 | :port 3001
7 | :env :dev}
8 |
--------------------------------------------------------------------------------
/src/mytomatoes/util.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.util
2 | (:require [cheshire.core :refer [generate-string]]))
3 |
4 | (defn result [r & [more]]
5 | {:status 200
6 | :body (generate-string (merge {:result r} more))
7 | :headers {"Content-Type" "application/json"}})
8 |
--------------------------------------------------------------------------------
/src/mytomatoes/server.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.server
2 | (:require [ring.adapter.jetty :as jetty]
3 | [taoensso.timbre :as timbre]))
4 | (timbre/refer-timbre)
5 |
6 | (defn create-and-start
7 | [handler & {:keys [port]}]
8 | {:pre [(not (nil? port))]}
9 | (let [server (jetty/run-jetty handler {:port port :join? false})]
10 | (info "Server started on port" port)
11 | server))
12 |
13 | (defn stop
14 | [server]
15 | (.stop server))
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mytomatoes.com
2 |
3 | mytomatoes.com helps you with the pomodoro technique by Francesco Cirillo - it's an online tomato kitchen timer and pomodoro tracker.
4 |
5 | ## Setting up development environment
6 |
7 | - Install mysql, memcached and leiningen.
8 | - Create empty database mytomatoes.
9 |
10 | Then start the app with `lein run`
11 |
12 | ## License
13 |
14 | Copyright © 2015-2021 Magnar Sveen
15 |
16 | Distributed under the Eclipse Public License either version 1.0 or (at your
17 | option) any later version.
18 |
--------------------------------------------------------------------------------
/resources/public/javascript/preferences.js:
--------------------------------------------------------------------------------
1 | /*global jQuery */
2 |
3 | var MT = MT || {};
4 |
5 | (function ($) {
6 |
7 | var toggle_preferences_pane = function () {
8 | $("#preferences").toggleClass("open");
9 | };
10 |
11 | var preference_changed = function () {
12 | MT.ajax_service.save_preference(this.name, this.checked);
13 | };
14 |
15 | MT.initialize_preferences = function () {
16 | $("#preferences h3").bind("click", toggle_preferences_pane);
17 | $("#preferences input:checkbox").bind("click", preference_changed);
18 | };
19 |
20 | }(jQuery));
--------------------------------------------------------------------------------
/resources/public/javascript/ajax_service.js:
--------------------------------------------------------------------------------
1 | /*global jQuery */
2 |
3 | var MT = MT || {};
4 |
5 | (function ($) {
6 |
7 | MT.ajax_service = {
8 |
9 | save_preference: function (name, value) {
10 | this.contact_server("actions/set_preference", {name: name, value: value});
11 | },
12 |
13 | contact_server: function (url_stub, params) {
14 | $.ajax({
15 | type: "POST",
16 | url: "/" + url_stub,
17 | data: params,
18 | dataType: "json"
19 | });
20 | }
21 | };
22 |
23 |
24 | }(jQuery));
--------------------------------------------------------------------------------
/src/mytomatoes/account.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.account
2 | (:require [pandect.core :refer [sha256]]))
3 |
4 | (def static-salt
5 | "*k*Pn9OR, ab5ec025e85ab1ab0de4bcab4b70068b4b3642fe, 062b1030-e63c-4f16-96bc-fd38dee78ae6OwBDZefhqlbYZ-wiIm+/N81l)V_(q-a5xD0IL4fzAFiRaxv9M39e87N_O*tog9+u, de5e6b220220759326851bc49cde941e576e9114, 18287807-d501-4c4c-9a13-cbcff7b75ee8(seRpitb!P=eSOCvd7@gbfH!c6oROD#OqRb7**EnBtlZn24fhzQp(U*(")
6 |
7 | (defn hash-password [password random-salt]
8 | (sha256 (str password "+" static-salt "+" random-salt)))
9 |
10 | (defn get-random-salt []
11 | (str (System/currentTimeMillis)))
12 |
13 | (defn password-matches? [account password]
14 | (= (:hashed-password account)
15 | (hash-password password (:random-salt account))))
16 |
--------------------------------------------------------------------------------
/src/mytomatoes/csv.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.csv
2 | (:require [clojure-csv.core :refer [write-csv]]
3 | [clj-time.format :refer [formatters unparse]]
4 | [mytomatoes.storage :refer [get-tomatoes]]))
5 |
6 | (def formatter (formatters :date-hour-minute-second))
7 |
8 | (defn- tomato-fields [tomato]
9 | [(unparse formatter (:local-start tomato))
10 | (unparse formatter (:local-end tomato))
11 | (:description tomato)])
12 |
13 | (defn render-tomatoes [{:keys [db session]}]
14 | {:body (let [tomatoes (get-tomatoes db (:account-id session))]
15 | (write-csv (map tomato-fields tomatoes) :force-quote true))
16 | :status 200
17 | :headers {"Content-Type" "text/csv; charset=utf-8"
18 | "Content-Disposition" "attachment; filename=mytomatoes.csv"}})
19 |
--------------------------------------------------------------------------------
/dev/user.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require [clj-time.core :as time]
3 | [clojure.java.io :as io]
4 | [clojure.pprint :refer [pprint pp]]
5 | [clojure.repl :refer :all]
6 | [clojure.set :as set]
7 | [clojure.string :as str]
8 | [clojure.tools.namespace.repl :refer [refresh]]
9 | [clojure.tools.trace :refer [trace-ns]]
10 | [mytomatoes.system]
11 | [mytomatoes.storage :as st]
12 | [print.foo :refer :all]
13 | [quick-reset.core :refer [stop reset system go]]
14 | [taoensso.timbre :as log]))
15 |
16 | (quick-reset.core/set-constructor 'mytomatoes.system/create-system)
17 |
18 | (log/merge-config! {:appenders {:spit (log/spit-appender {:fname "debug.log"})}})
19 |
20 | (comment
21 | (def db {:connection (:db system)})
22 |
23 | (st/get-account db "magnars")
24 |
25 | (.getYear (:local-start (first (st/get-tomatoes db 1))))
26 |
27 |
28 |
29 | )
30 |
--------------------------------------------------------------------------------
/resources/public/javascript/animation.js:
--------------------------------------------------------------------------------
1 | /*global jQuery */
2 | (function ($) {
3 |
4 | $.fn.highlight = function () {
5 | var original_color = this.css("background-color");
6 | $(this).css("background-color", "#ffcb6e").animate({backgroundColor: original_color}, 1000);
7 | };
8 |
9 | $.fn.flash = function (message) {
10 | var original_color = this.css("color");
11 | this.css("color", "#ffcb6e").text(message).animate({color: original_color}, 3000);
12 | };
13 |
14 | $.fn.flash_background = function (color) {
15 | var that = this, original_color = this.css("background-color"), fade_to_new, fade_to_original;
16 |
17 | fade_to_original = function () {
18 | that.animate({backgroundColor: original_color}, 500, fade_to_new);
19 | };
20 |
21 | fade_to_new = function () {
22 | that.animate({backgroundColor: color}, 500, fade_to_original);
23 | };
24 |
25 | fade_to_new();
26 | };
27 |
28 | })(jQuery);
--------------------------------------------------------------------------------
/resources/public/ga.js:
--------------------------------------------------------------------------------
1 | if (!MT.debug) {
2 | var _gaq = _gaq || [];
3 | _gaq.push(['_setAccount', 'UA-16879106-1']);
4 | _gaq.push(['_trackPageview']);
5 |
6 | (function() {
7 | var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
8 | ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
9 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
10 | })();
11 |
12 | var pageTracker;
13 | jQuery(document).ajaxSuccess(function (event, xhr, options) {
14 | if (window._gat) {
15 | if (!pageTracker) {
16 | pageTracker = _gat._getTracker('UA-16879106-1');
17 | }
18 | pageTracker._trackPageview(options.url);
19 | }
20 | });
21 |
22 | window.onerror = function(message, file, line) {
23 | var sFormattedMessage = '[' + file + ' (' + line + ')] ' + message;
24 | _gaq.push(['_trackEvent', 'Exceptions', 'Application', sFormattedMessage, null, true]);
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/resources/public/javascript/external/jquery.url.js:
--------------------------------------------------------------------------------
1 | /*
2 | jQuery Url Plugin
3 | * Version 1.0
4 | * 2009-03-22 19:30:05
5 | * URL: http://ajaxcssblog.com/jquery/url-read-get-variables/
6 | * Description: jQuery Url Plugin gives the ability to read GET parameters from the actual URL
7 | * Author: Matthias Jäggli
8 | * Copyright: Copyright (c) 2009 Matthias Jäggli
9 | * Licence: dual, MIT/GPLv2
10 | */
11 | (function ($) {
12 | $.url = {};
13 | $.extend($.url, {
14 | _params: {},
15 | init: function(){
16 | var paramsRaw = "";
17 | try{
18 | paramsRaw =
19 | (document.location.href.split("?", 2)[1] || "").split("#")[0].split("&") || [];
20 | for(var i = 0; i< paramsRaw.length; i++){
21 | var single = paramsRaw[i].split("=");
22 | if(single[0])
23 | this._params[single[0]] = unescape(single[1]);
24 | }
25 | }
26 | catch(e){
27 | alert(e);
28 | }
29 | },
30 | param: function(name){
31 | return this._params[name] || "";
32 | },
33 | paramAll: function(){
34 | return this._params;
35 | }
36 | });
37 | $.url.init();
})(jQuery);
38 |
--------------------------------------------------------------------------------
/src/mytomatoes/pages/error.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.pages.error
2 | (:require [optimus.link :refer [file-path]]
3 | [hiccup.core :refer [html]]
4 | [mytomatoes.layout :refer [with-layout]]))
5 |
6 | (def error-html
7 | (html [:p
8 | "Bah! Something's not right. We've screwed up. An angry email has
9 | been sent to the admin (unless that's broken aswell, you know,
10 | Murphys Law and all). Feel free to yell at us on "
11 | [:a {:href "http://twitter.com/mytomatoes" :target "_blank"}
12 | "@mytomatoes"]
13 | ", if that makes you feel better. Or you can "
14 | [:a {:href "/"}
15 | "try again from the beginning"] "."]))
16 |
17 | (defn get-page [request]
18 | (if (:optimus-assets request)
19 | (with-layout request
20 | {:body
21 | (html [:div {:id "error"}
22 | error-html
23 | [:img {:src (file-path request "/theme/images/error.gif")}]])
24 | :status 500})
25 | {:headers {"Content-Type" "text/html; charset=utf-8"}
26 | :status 500
27 | :body error-html}))
28 |
--------------------------------------------------------------------------------
/src/mytomatoes/pages/login.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.pages.login
2 | (:require [mytomatoes.layout :refer [with-layout]]
3 | [hiccup.core :refer [html]]))
4 |
5 | (defn get-page [request]
6 | (with-layout request
7 | {:body
8 | (html
9 | [:div {:id "welcome"}
10 | (if (get-in request [:params "session"])
11 | [:p {:id "session_expired"} "sorry, your old session is lost to the ages"]
12 | [:p "mytomatoes.com helps you with the "
13 | [:a {:href "http://www.pomodorotechnique.com/"}
14 | "pomodoro technique"]
15 | " by "
16 | [:a {:href "http://francescocirillo.com/"}
17 | "Francesco Cirillo"]
18 | " - it's an online tomato kitchen timer and pomodoro tracker."])
19 | [:form {:action "register"}
20 | [:a {:id "toggle_register_login" :href "#"}
21 | "already registered?"]
22 | [:h3 "register"]
23 | [:div {:id "fields"}
24 | [:input {:type "text" :id "username" :name "username"}]
25 | [:input {:type "password" :id "password" :name "password"}]
26 | [:input {:type "password" :id "password2" :name "password2"}]]
27 | [:input {:type "submit" :id "submit" :value "loading..." :disabled true}]
28 | [:div {:id "remember_me"}
29 | [:input {:type "checkbox" :id "remember" :name "remember" :checked true}]
30 | [:label {:for "remember"} " remember me"]]]])
31 | :script-bundles ["login.js"]}))
32 |
--------------------------------------------------------------------------------
/src/mytomatoes/migrations.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.migrations
2 | (:require [taoensso.timbre :as timbre]
3 | [yesql.core :refer [defqueries]]))
4 | (timbre/refer-timbre)
5 |
6 | (defqueries "migrations.sql")
7 |
8 | (defmacro migration [db version num f]
9 | `(when (> ~num ~version)
10 | (info ~(str "Running migration " num ": " f))
11 | (~f {} ~db)))
12 |
13 | (defn- delete-tomatoes-with-invalid-dates! [_ db]
14 | (delete-tomatoes-with-invalid-start-dates! {:date "0000-00-00 00:00:00"} db)
15 | (delete-tomatoes-with-invalid-end-dates! {:date "0000-00-00 00:00:00"} db))
16 |
17 | (def latest 10)
18 |
19 | (defn migrate! [db]
20 | (when (empty? (check-for-schema-info {} db))
21 | (create-table-schema-info! {} db)
22 | (set-initial-schema-version! {} db))
23 | (let [version (-> (get-version {} db) first :version)]
24 | (migration db version 1 create-table-accounts!)
25 | (migration db version 2 create-table-event-log!)
26 | (migration db version 3 create-table-remember-codes!)
27 | (migration db version 4 create-table-tomatoes!)
28 | (migration db version 5 create-table-preferences!)
29 | (migration db version 6 drop-event-log!)
30 | (migration db version 7 add-account-id-index-to-tomatoes!)
31 | (migration db version 8 add-username-index-to-accounts!)
32 | (migration db version 9 drop-tomatoes-updated-at-column!)
33 | (migration db version 10 delete-tomatoes-with-invalid-dates!)
34 |
35 | (when (> latest version)
36 | (info "Updated system to newest version:" latest)
37 | (update-version! {:version latest} db))))
38 |
--------------------------------------------------------------------------------
/resources/public/theme/css/reset.css:
--------------------------------------------------------------------------------
1 | /*
2 | * YUI CSS Reset
3 | *
4 | * Copyright (c) 2006, Yahoo! Inc. All rights reserved.
5 | * Code licensed under the BSD License:
6 | * http://developer.yahoo.net/yui/license.txt
7 | * version: 0.12.1
8 | */
9 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td {
10 | margin: 0;
11 | padding: 0;
12 | }
13 |
14 | table {
15 | border-collapse: collapse;
16 | border-spacing: 0;
17 | }
18 |
19 | fieldset,img {
20 | border: 0;
21 | }
22 |
23 | address,caption,cite,code,dfn,em,strong,th,var {
24 | font-style: normal;
25 | font-weight: normal;
26 | }
27 |
28 | ol,ul {
29 | list-style: none;
30 | }
31 |
32 | caption,th {
33 | text-align: left;
34 | }
35 |
36 | h1,h2,h3,h4,h5,h6 {
37 | font-size: 100%;
38 | font-weight: normal;
39 | }
40 |
41 | q:before,q:after {
42 | content: '';
43 | }
44 |
45 | abbr,acronym {
46 | border: 0;
47 | }
48 |
49 | /*
50 | Copyright (c) 2007, Yahoo! Inc. All rights reserved.
51 | Code licensed under the BSD License:
52 | http://developer.yahoo.net/yui/license.txt
53 | version: 2.4.1
54 | */
55 |
56 | /**
57 | * Percents could work for IE, but for backCompat purposes, we are using keywords.
58 | * x-small is for IE6/7 quirks mode.
59 | */
60 | body {
61 | font: 13px/1.231 arial,helvetica,clean,sans-serif;
62 | *font-size:small;
63 | *font:x-small;
64 | }
65 |
66 | table {
67 | font-size:inherit;
68 | font:100%;
69 | }
70 |
71 | /**
72 | * Bump up IE to get to 13px equivalent
73 | */
74 | pre,code,kbd,samp,tt {
75 | font-family:monospace;
76 | *font-size:108%;
77 | line-height:100%;
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject mytomatoes "0.1.0-SNAPSHOT"
2 | :description "The mytomatoes.com site, retro style"
3 | :url "http://mytomatoes.com"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :jvm-opts ["-Xmx1G"
7 | "-Djava.awt.headless=true"
8 | "-Dfile.encoding=UTF-8"]
9 | :dependencies [[org.clojure/clojure "1.7.0"]
10 | [clj-time "0.11.0"]
11 | [ring "1.4.0" :exclusions [org.clojure/java.classpath]]
12 | [hiccup "1.0.5"]
13 | [optimus "0.18.3"]
14 | [compojure "1.4.0"]
15 | [pandect "0.5.4"]
16 | [crypto-random "1.2.0"]
17 | [commons-lang/commons-lang "2.6"]
18 | [yesql "0.5.1"]
19 | [mysql/mysql-connector-java "5.1.6"]
20 | [org.postgresql/postgresql "9.3-1102-jdbc41"]
21 | [com.taoensso/timbre "4.1.4"]
22 | [inflections "0.10.0"]
23 | [clojure-csv "2.0.1"]
24 | [org.clojure/tools.nrepl "0.2.12"]
25 | [com.postspectacular/rotor "0.1.0"]
26 | [cheshire "5.5.0"]
27 | [ring-session-memcached "0.0.1"]
28 | [org.slf4j/slf4j-nop "1.7.7"]
29 | [com.draines/postal "1.11.4"]]
30 | :main mytomatoes.system
31 | :profiles {:dev {:dependencies [[org.clojure/tools.trace "0.7.9"]
32 | [ciderale/quick-reset "0.2.0"]
33 | [print-foo "1.0.2"]
34 | [prone "0.8.2"]]
35 | :main user
36 | :source-paths ["dev"]}
37 | :uberjar {:aot :all}})
38 |
--------------------------------------------------------------------------------
/resources/public/javascript/input_hints.js:
--------------------------------------------------------------------------------
1 | /*global jQuery */
2 | (function ($) {
3 | var add_hint_to = {};
4 |
5 | add_hint_to.text = function (field, hint) {
6 | field = $(field);
7 | function show_hint() {
8 | if (field.val() === "" || field.val() === hint) {
9 | field.val(hint).addClass("hint");
10 | }
11 | }
12 | function hide_hint() {
13 | if (field.val() === hint) {
14 | field.val("").removeClass("hint");
15 | }
16 | }
17 | field.blur(show_hint).focus(hide_hint).each(show_hint);
18 | };
19 |
20 | add_hint_to.password = function (field, hint) {
21 | var real = $(field), dummy = $(" ").insertBefore(real).val(hint).addClass("hint");
22 | function show_hint() {
23 | if (real.val() === "") {
24 | real.hide();
25 | dummy.show();
26 | }
27 | }
28 | function hide_hint() {
29 | dummy.hide();
30 | real.show().focus();
31 | }
32 | real.data("hinted-password-field", true);
33 | real.blur(show_hint);
34 | dummy.focus(hide_hint);
35 | show_hint();
36 | };
37 |
38 | $.fn.hide_password_field = function () {
39 | this.hide().prev().hide();
40 | };
41 |
42 | $.fn.show_password_field = function () {
43 | var real = this, dummy = real.prev();
44 | if (real.val() === "") {
45 | dummy.show();
46 | } else {
47 | real.show();
48 | }
49 | };
50 |
51 | $.fn.add_hint = function (hint) {
52 | this.filter("input").each(function () {
53 | add_hint_to[this.type](this, hint);
54 | });
55 | };
56 |
57 | })(jQuery);
--------------------------------------------------------------------------------
/resources/queries.sql:
--------------------------------------------------------------------------------
1 | -- name: account-by-username
2 | -- Returns the account details for a given username
3 | SELECT id, username, hashed_password, random_salt
4 | FROM Accounts WHERE username LIKE :username
5 |
6 | -- name: insert-accountthese aren't equal (they should be)").insertAfter("#password").hide().fadeIn(500);
13 | if ($.browser.opera) {
14 | error.css("margin-top", "5px");
15 | }
16 | $("#password, #password2").keyup(function () {
17 | var p1 = $("#password").val(), p2 = $("#password2").val();
18 | if (p1 === p2) {
19 | error.fadeOut(500, function () {
20 | error.remove();
21 | });
22 | $("#password, #password2").unbind("keyup");
23 | }
24 | });
25 | }
26 |
27 | function failure(json) {
28 | switch (json.result) {
29 | case "wrong_code":
30 | location.href = "/recovery?code=invalid";
31 | return true;
32 | case "missing_password":
33 | $("#password").show_validation_error("write your new password here").prev().trigger("focus").next().focus();
34 | return true;
35 | case "mismatched_passwords":
36 | show_mismatched_password_validation();
37 | return true;
38 | default:
39 | return false;
40 | }
41 | }
42 |
43 | function submit_form(event) {
44 | var form = $(this);
45 | event.preventDefault();
46 | $.postJSON("actions/change-password", form.serializeArray(), big_success, failure);
47 | return false;
48 | }
49 |
50 | function initialize() {
51 | $("#password").add_hint("password");
52 | $("#password2").add_hint("password again");
53 | $("#welcome form").submit(submit_form);
54 | $("#submit").val("this one I'll remember").attr("disabled", "");
55 | }
56 |
57 | initialize();
58 |
59 | }(jQuery));
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/mytomatoes/storage.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.storage
2 | (:require [clj-time.coerce :refer [from-date to-local-date]]
3 | [clojure.string :as str]
4 | [mytomatoes.account :refer [get-random-salt hash-password]]
5 | [yesql.core :refer [defqueries]]))
6 |
7 | (defqueries "queries.sql")
8 |
9 | (defn account-exists? [db username]
10 | (seq (account-by-username {:username username} db)))
11 |
12 | (defn create-account! [db username password]
13 | (let [salt (get-random-salt)]
14 | (-> (insert-accountkeyword [s]
43 | (keyword (str/replace s "_" "-")))
44 |
45 | (defn get-preferences [db account-id]
46 | (->> (account-preferences {:account_id account-id} db)
47 | (map (fn [p] [(->keyword (:name p))
48 | (if (= "false" (:value p))
49 | false (:value p))]))
50 | (into {})))
51 |
52 | (defn get-account-id-by-remember-code [db code]
53 | (:account_id (first (account-by-remember-code {:code code} db))))
54 |
--------------------------------------------------------------------------------
/src/mytomatoes/login.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.login
2 | (:require [clj-time.core :as time]
3 | [crypto.random]
4 | [mytomatoes.storage :as s]
5 | [mytomatoes.util :refer [result]]
6 | [ring.util.response :refer [redirect]]
7 | [taoensso.timbre :refer [info]])
8 | (:import [org.apache.commons.codec.binary Hex]))
9 |
10 | (defn json-request? [req]
11 | (re-find #"application/json" (get-in req [:headers "accept"])))
12 |
13 | (defn redirect-if-not-logged-in [handler]
14 | (fn [req]
15 | (if (:account-id (:session req))
16 | (handler req)
17 | (if (json-request? req)
18 | (result "not_logged_in")
19 | (redirect "/?session=expired")))))
20 |
21 | (defn redirect-if-logged-in [handler]
22 | (fn [req]
23 | (if (:account-id (:session req))
24 | (if (json-request? req)
25 | (result "already_logged_in")
26 | (redirect "/"))
27 | (handler req))))
28 |
29 | (defn generate-auth-token
30 | []
31 | (-> (crypto.random/bytes 32)
32 | (Hex/encodeHex)
33 | (String.)))
34 |
35 | (defn log-in-with-remember-code [db remember-code]
36 | (when-let [account-id (s/get-account-id-by-remember-code db remember-code)]
37 | (s/remove-remember-code! {:code remember-code} db)
38 | account-id))
39 |
40 | (def a-year (* 60 60 24 356))
41 |
42 | (defn remember! [response db account-id]
43 | (let [code (generate-auth-token)]
44 | (s/add-remember-code! {:account_id account-id
45 | :code code} db)
46 | (assoc-in response [:cookies "mytomatoes_remember"]
47 | {:value code
48 | :path "/"
49 | :http-only true
50 | :expires (time/plus (time/now) (time/years 1))})))
51 |
52 | (defn wrap-remember-code [handler]
53 | (fn [req]
54 | (if-not (:account-id (:session req))
55 | (if-let [remember-code (get-in req [:cookies "mytomatoes_remember" :value])]
56 | (if-let [account-id (log-in-with-remember-code (:db req) remember-code)]
57 | (do
58 | (info "Logged in account with id" account-id "using remember code.")
59 | (-> (handler (assoc-in req [:session :account-id] account-id))
60 | (assoc-in [:session :account-id] account-id)
61 | (remember! (:db req) account-id)))
62 | (handler req))
63 | (handler req))
64 | (handler req))))
65 |
--------------------------------------------------------------------------------
/src/mytomatoes/system.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.system
2 | (:gen-class :main true)
3 | (:require [clojure.java.io :as io]
4 | [clojure.tools.nrepl.server :as nrepl]
5 | [com.postspectacular.rotor :as rotor]
6 | [mytomatoes.migrations :refer [migrate!]]
7 | [mytomatoes.server :as server]
8 | [mytomatoes.web :as web]
9 | [taoensso.timbre.appenders.3rd-party.rolling :as rolling-appender]
10 | [taoensso.timbre.appenders.postal :as postal-appender]
11 | [taoensso.timbre :as log]))
12 |
13 | (defn send-error-emails [{:keys [host port user pass from to]}]
14 | (log/merge-config!
15 | {:appenders {:postal
16 | (postal-appender/postal-appender
17 | ^{:host host :port port :user user :pass pass}
18 | {:from from :to to})}})
19 | (log/info "Sending errors to" to "via" host))
20 |
21 | (defn write-logs-to-file [conf]
22 | (log/merge-config!
23 | {:appenders {:rolling
24 | (rolling-appender/rolling-appender conf)}})
25 | (log/info "Writing logs to" (pr-str (:path conf)) (name (:pattern conf :daily))))
26 |
27 | (defn start
28 | "Performs side effects to initialize the system, acquire resources,
29 | and start it running. Returns an updated instance of the system."
30 | [system]
31 | (migrate! {:connection (:db system)})
32 | (let [handler (web/create-app {:connection (:db system)}
33 | (:memcached system)
34 | (:env system))
35 | server (server/create-and-start handler :port (:port system))]
36 | (assoc system
37 | :handler handler
38 | :server server)))
39 |
40 | (defn stop
41 | "Performs side effects to shut down the system and release its
42 | resources. Returns an updated instance of the system."
43 | [system]
44 | (when (:server system)
45 | (server/stop (:server system)))
46 | (dissoc system :handler :server))
47 |
48 | (defn create-system
49 | "Returns a new instance of the whole application."
50 | []
51 | (merge (read-string (slurp (io/resource "mytomatoes-config.edn")))
52 | {:start start
53 | :stop stop}))
54 |
55 | (defn -main [& args]
56 | (let [system (create-system)]
57 | (start system)
58 |
59 | (when (:rotor-log-file system)
60 | (write-logs-to-file {:path (:rotor-log-file system)
61 | :pattern :weekly}))
62 |
63 | (when (:mail system)
64 | (send-error-emails (:mail system)))
65 |
66 | (let [repl (nrepl/start-server :port (:repl-port system 0) :bind "127.0.0.1")]
67 | (log/info "Repl started at" (:port repl)))))
68 |
--------------------------------------------------------------------------------
/resources/public/javascript/countdown.js:
--------------------------------------------------------------------------------
1 | /*global jQuery, setTimeout, document */
2 | (function ($) {
3 | $.fn.countdown = function (total_seconds, callback) {
4 | var that = this;
5 | var countdown_active = true;
6 | var original_title = document.title;
7 | var seconds_left = total_seconds;
8 | var start_time;
9 |
10 | function now() {
11 | return new Date().getTime();
12 | }
13 |
14 | function calculate_seconds_left() {
15 | return Math.round((start_time - now()) / 1000) + total_seconds;
16 | }
17 |
18 | function minutes_left() {
19 | return Math.floor((seconds_left + 6) / 60);
20 | }
21 |
22 | function rounded_minutes_left() {
23 | return Math.floor((seconds_left + 40) / 60) + " min";
24 | }
25 |
26 | function tenth_of_seconds_left() {
27 | return Math.floor((seconds_left - 60 * minutes_left() + 6) / 10) + "0";
28 | }
29 |
30 | function rounded_seconds_left() {
31 | return minutes_left() + ":" + tenth_of_seconds_left();
32 | }
33 |
34 | function time_left() {
35 | if (seconds_left > 90) {
36 | return rounded_minutes_left();
37 | } else if (seconds_left > 15) {
38 | return rounded_seconds_left();
39 | } else {
40 | return "0:" + (seconds_left > 9 ? seconds_left : "0" + seconds_left);
41 | }
42 | }
43 |
44 | function update_display() {
45 | var display_text = time_left();
46 | if (that.text() !== display_text) {
47 | that.text(display_text);
48 | document.title = display_text + " - " + original_title;
49 | }
50 | }
51 |
52 | function count_down() {
53 | if (! countdown_active) {
54 | if (original_title) {
55 | document.title = original_title;
56 | }
57 | return;
58 | }
59 | seconds_left = calculate_seconds_left();
60 | update_display();
61 | if (seconds_left > 0) {
62 | setTimeout(count_down, 1000);
63 | } else {
64 | document.title = original_title;
65 | callback();
66 | }
67 | }
68 |
69 | if ($.cancel_countdown) {
70 | $.cancel_countdown();
71 | }
72 | $.cancel_countdown = function () {
73 | countdown_active = false;
74 | document.title = original_title;
75 | original_title = false;
76 | };
77 |
78 | start_time = now();
79 | update_display();
80 | setTimeout(count_down, 1000);
81 | };
82 |
83 | })(jQuery);
--------------------------------------------------------------------------------
/resources/migrations.sql:
--------------------------------------------------------------------------------
1 | -- name: check-for-schema-info
2 | SHOW TABLES LIKE 'schema_info'
3 |
4 | -- name: create-table-schema-info!
5 | CREATE TABLE schema_info (version INT, PRIMARY KEY (version))
6 |
7 | -- name: set-initial-schema-version!
8 | INSERT INTO schema_info VALUES (0)
9 |
10 | -- name: create-table-accounts!
11 | CREATE TABLE Accounts (
12 | id INT UNSIGNED NOT NULL AUTO_INCREMENT,
13 | username VARCHAR(255) NOT NULL,
14 | hashed_password VARCHAR(255) NOT NULL,
15 | random_salt VARCHAR(255) NOT NULL,
16 | updated_at TIMESTAMP,
17 | created_at TIMESTAMP,
18 | PRIMARY KEY (id))
19 |
20 | -- name: create-table-event-log!
21 | CREATE TABLE EventLog (
22 | id INT UNSIGNED NOT NULL AUTO_INCREMENT,
23 | event VARCHAR(255) NOT NULL,
24 | ip_address VARCHAR(15) NOT NULL,
25 | account_id INT UNSIGNED,
26 | details VARCHAR(255),
27 | time TIMESTAMP,
28 | PRIMARY KEY (id))
29 |
30 | -- name: create-table-remember-codes!
31 | CREATE TABLE RememberCodes (
32 | code VARCHAR(64) NOT NULL,
33 | account_id INT UNSIGNED NOT NULL,
34 | created_at TIMESTAMP NOT NULL,
35 | PRIMARY KEY (code))
36 |
37 | -- name: create-table-tomatoes!
38 | CREATE TABLE Tomatoes (
39 | id INT UNSIGNED NOT NULL AUTO_INCREMENT,
40 | account_id INT UNSIGNED NOT NULL,
41 | status ENUM ('started', 'completed', 'squashed') NOT NULL DEFAULT 'started',
42 | description VARCHAR(255),
43 | local_start TIMESTAMP,
44 | local_end TIMESTAMP,
45 | created_at TIMESTAMP,
46 | updated_at TIMESTAMP,
47 | PRIMARY KEY (id))
48 |
49 | -- name: create-table-preferences!
50 | CREATE TABLE Preferences (
51 | account_id MEDIUMINT UNSIGNED NOT NULL,
52 | name VARCHAR(255) NOT NULL,
53 | value VARCHAR(255),
54 | PRIMARY KEY (account_id, name))
55 |
56 | -- name: get-version
57 | SELECT version FROM schema_info
58 |
59 | -- name: update-version!
60 | UPDATE schema_info SET version = :version
61 |
62 | -- name: add-account-id-index-to-tomatoes!
63 | CREATE INDEX tomatoes_account_index ON Tomatoes (account_id)
64 |
65 | -- name: add-username-index-to-accounts!
66 | CREATE INDEX accounts_username_index ON Accounts (username)
67 |
68 | -- name: drop-event-log!
69 | DROP TABLE EventLog;
70 |
71 | -- name: drop-tomatoes-updated-at-column!
72 | ALTER TABLE Tomatoes DROP COLUMN updated_at;
73 |
74 | -- name: delete-tomatoes-with-invalid-start-dates!
75 | DELETE FROM Tomatoes WHERE local_start = :date
76 |
77 | -- name: delete-tomatoes-with-invalid-end-dates!
78 | DELETE FROM Tomatoes WHERE local_end = :date
79 |
--------------------------------------------------------------------------------
/src/mytomatoes/web.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.web
2 | (:require [compojure.core :refer [routes GET POST wrap-routes]]
3 | [mytomatoes.actions :as actions]
4 | [mytomatoes.csv :as csv]
5 | [mytomatoes.layout :as layout]
6 | [mytomatoes.login :refer [redirect-if-not-logged-in wrap-remember-code redirect-if-logged-in]]
7 | [mytomatoes.pages.error :as error]
8 | [mytomatoes.pages.home :as home]
9 | [mytomatoes.pages.login :as login]
10 | [mytomatoes.pages.recovery :as recovery]
11 | [optimus.optimizations :as optimizations]
12 | [optimus.prime :as optimus]
13 | [optimus.strategies :as strategies]
14 | [ring.middleware.content-type]
15 | [ring.middleware.cookies]
16 | [ring.middleware.not-modified]
17 | [ring.middleware.params]
18 | [ring.middleware.session]
19 | [ring.middleware.session.memcached :refer [mem-store]]
20 | [ring.util.response :as res]
21 | [taoensso.timbre :refer [info error]]))
22 |
23 | (defn app-routes []
24 | (routes
25 | (GET "/" request (if (:account-id (:session request))
26 | (home/get-page request)
27 | (login/get-page request)))
28 | (GET "/error" request (error/get-page request))
29 | (wrap-routes
30 | (routes
31 | (GET "/recovery" request (recovery/get-page request))
32 | (GET "/change-password" request (recovery/get-change-password-page request))
33 | (POST "/actions/check-my-words" request (actions/check-my-words request))
34 | (POST "/actions/register" request (actions/register request))
35 | (POST "/actions/login" request (actions/login request))
36 | (POST "/actions/change-password" request (actions/change-password request)))
37 | redirect-if-logged-in)
38 | (wrap-routes
39 | (routes
40 | (POST "/actions/set_preference" request (actions/set-preference request))
41 | (POST "/actions/logout" [] (actions/logout))
42 | (POST "/actions/keep_session_alive" [] (actions/keep-session-alive))
43 | (POST "/actions/complete_tomato" request (actions/complete-tomato request))
44 | (GET "/views/completed_tomatoes" request (home/completed-tomatoes-fragment request))
45 | (GET "/views/yearly_tomatoes/:year" [year :as request] (home/yearly-tomatoes request (Integer/parseInt year)))
46 | (GET "/tomatoes.csv" request (csv/render-tomatoes request)))
47 | redirect-if-not-logged-in)))
48 |
49 | (defn include-stuff-in-request [handler db env]
50 | (fn [req]
51 | (handler (assoc req :db db :env env))))
52 |
53 | (defn wrap-exceptions [handler]
54 | (fn [req]
55 | (try (handler req)
56 | (catch Exception e
57 | (error e)
58 | (error/get-page req)))))
59 |
60 | (defn create-app [db memcached env]
61 | (-> (app-routes)
62 | (wrap-remember-code)
63 | (include-stuff-in-request db env)
64 | (ring.middleware.params/wrap-params)
65 | (ring.middleware.session/wrap-session {:store (mem-store memcached)})
66 | (wrap-exceptions)
67 | (optimus/wrap layout/get-assets
68 | (if (= :prod env) optimizations/all optimizations/none)
69 | (if (= :prod env) strategies/serve-frozen-assets strategies/serve-live-assets))
70 | (ring.middleware.content-type/wrap-content-type)
71 | (ring.middleware.not-modified/wrap-not-modified)
72 | (wrap-exceptions)))
73 |
--------------------------------------------------------------------------------
/src/mytomatoes/pages/recovery.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.pages.recovery
2 | (:require [mytomatoes.layout :refer [with-layout]]
3 | [hiccup.core :refer [html]]
4 | [cheshire.core :refer [generate-string]]
5 | [mytomatoes.word-stats :refer [common-words]]
6 | [ring.util.response :refer [redirect]]
7 | [mytomatoes.storage :as st]
8 | [taoensso.timbre :refer [info]]))
9 |
10 | (def map-of-common-words
11 | (->> common-words
12 | (map (juxt identity (constantly 1)))
13 | (into {})))
14 |
15 | (defn get-page [request]
16 | (let [proposed-username (get-in request [:params "username"] "")
17 | username (if (= proposed-username "username") "" proposed-username)]
18 | (with-layout request
19 | {:body
20 | (html
21 | [:div {:id "welcome" :class "recovery"}
22 | (if (= "invalid" (get-in request [:params "code"]))
23 | [:p "Whoa, that code to change your password was wrong, for some
24 | reason. Damn. Let's try again. Type in four words that you've used
25 | in your tomatoes. If they all match, we'll call that good enough.
26 | Okay?"]
27 | (list
28 | [:p "Lost your password? Let's see if we can fix that. Type in four
29 | words that you've used in your tomatoes. If they all match, we'll
30 | call that good enough. Okay?"]
31 | [:p "Think back. What have you been doing? What sort of description
32 | did you write for your tomatoes? Did you use any special names?
33 | Abbreviations? Words specific to your field? Type them in one at a
34 | time."]))
35 | [:form {:id "the-form"}
36 | [:div {:id "fields"}
37 | [:div {:class "mas"} [:label.strong {:for "username"} "your username:"]]
38 | [:input {:type "text" :id "username" :name "username" :value username}]
39 | [:div {:class "mas"} [:label.strong {:for "word1"} "four words:"]]
40 | [:input {:type "text" :id "word1" :name "word1" :class "word"}]
41 | [:input {:type "text" :id "word2" :name "word2" :class "word"}]
42 | [:input {:type "text" :id "word3" :name "word3" :class "word"}]
43 | [:input {:type "text" :id "word4" :name "word4" :class "word"}]]
44 | [:input {:type "submit" :id "submit" :value "loading..." :disabled true}]]]
45 | [:script
46 | "var MT = {};"
47 | "MT.commonWords = " (generate-string map-of-common-words) ";"])
48 | :script-bundles ["recovery.js"]})))
49 |
50 | (defn get-change-password-page [request]
51 | (if-let [code (get-in request [:params "code"])]
52 | (if-let [account-id (st/get-account-id-by-remember-code (:db request) code)]
53 | (with-layout request
54 | {:body
55 | (html
56 | [:div {:id "welcome"}
57 | [:p "Please type your new password a couple times, and we can get back to doing some real work. :-)"]
58 | [:form {:id "the-form"}
59 | [:input {:type "hidden" :name "code" :value code}]
60 | [:h3 "Change password"]
61 | [:div {:id "fields"}
62 | [:input {:type "password" :id "password" :name "password"}]
63 | [:input {:type "password" :id "password2" :name "password2"}]]
64 | [:input {:type "submit" :id "submit" :value "loading..." :disabled true}]]])
65 | :script-bundles ["change-password.js"]})
66 | (do
67 | (info "Change password page visited with invalid code: " code)
68 | (redirect "/recovery?code=invalid")))
69 | (do
70 | (info "Change password page visited without a code.")
71 | (redirect "/recovery"))))
72 |
--------------------------------------------------------------------------------
/resources/public/javascript/library.js:
--------------------------------------------------------------------------------
1 | /*global MT, jQuery, location, alert
2 | */
3 | var MT = MT || {};
4 |
5 | (function ($) {
6 | MT.debug = $.url.param("debug") || $.browser.mozilla && location.host === "local.mytomatoes.com:3001";
7 |
8 | function today() {
9 | return new Date().moveToMidnight();
10 | }
11 |
12 | MT.today = today;
13 |
14 | function yesterday() {
15 | return today().addDays(-1);
16 | }
17 |
18 | function fix_day_name() {
19 | var header = $(this),
20 | day = $(this).find("strong"),
21 | date = Date.parseExact(day.text(), "yyyy-MM-dd");
22 | if (date === null) {
23 | return; // do nothing
24 | } else if (date.equals(today())) {
25 | header.attr("id", "today");
26 | day.text("today");
27 | } else if (date.equals(yesterday())) {
28 | day.text("yesterday");
29 | } else {
30 | day.text(date.toString("dddd dd. MMM").toLowerCase());
31 | }
32 | header.data("date", date);
33 | }
34 |
35 | MT.fix_day_names = function () {
36 | $(".done h3").each(fix_day_name);
37 | };
38 |
39 | MT.reload_done_div = function () {
40 | $("#done").load("views/completed_tomatoes", MT.fix_day_names);
41 | };
42 |
43 | MT.make_sure_that_today_is_still_today = function () {
44 | if ($("#today").exists() && ! $("#today").data("date").equals(today())) {
45 | MT.reload_done_div();
46 | }
47 | };
48 |
49 | $.postJSON = function (url, parameters, callback, error_callback) {
50 | var handle_error = function (err) {
51 | if (error_callback && error_callback(err)) {
52 | // handled by callback, do nothing
53 | } else {
54 | location.href = 'error';
55 | }
56 | };
57 | $.ajax({
58 | data: parameters,
59 | type: "POST",
60 | url: url,
61 | timeout: 20000,
62 | dataType: 'json',
63 | error: handle_error,
64 | success: function (json) {
65 | if (json.result === "ok") {
66 | if (callback) {
67 | callback(json);
68 | }
69 | } else if (json.result === "not_logged_in") {
70 | location.href = location.href + "?session=expired";
71 | } else {
72 | handle_error(json);
73 | }
74 | }
75 | });
76 | };
77 |
78 | $.fn.exists = function () {
79 | return this.length > 0;
80 | };
81 |
82 | Date.prototype.toTimestamp = function () {
83 | return this.toString("yyyy-MM-dd HH:mm:ss");
84 | };
85 |
86 | Date.prototype.toClock = function () {
87 | return this.toString("HH:mm");
88 | };
89 |
90 | Date.prototype.to12hrClock = function () {
91 | return this.toString("hh:mm tt");
92 | };
93 |
94 | Date.prototype.moveToMidnight = function () {
95 | return Date.parseExact(this.toString("yyyy-MM-dd"), "yyyy-MM-dd");
96 | };
97 |
98 | $.fn.show_validation_error = function (text) {
99 | if ($(this).next().is(".validation_error")) {
100 | $(this).next().html(text);
101 | return this;
102 | }
103 | $("
" + text + "
").insertAfter(this).hide().fadeIn(500);
104 | $(this).one("keydown", function () {
105 | $(this).next().filter(".validation_error").fadeOut(1000, function () {
106 | $(this).remove();
107 | });
108 | });
109 | return this;
110 | };
111 |
112 | })(jQuery);
113 |
--------------------------------------------------------------------------------
/resources/public/javascript/external/jquery.color.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery Color Animations
3 | * Copyright 2007 John Resig
4 | * Released under the MIT and GPL licenses.
5 | */
6 |
7 | (function(jQuery){
8 |
9 | // We override the animation for all of these color styles
10 | jQuery.each(['backgroundColor', 'borderBottomColor', 'borderLeftColor', 'borderRightColor', 'borderTopColor', 'color', 'outlineColor'], function(i,attr){
11 | jQuery.fx.step[attr] = function(fx){
12 | if ( fx.state <= 0.045 ) {
13 | fx.start = getColor( fx.elem, attr );
14 | fx.end = getRGB( fx.end );
15 | }
16 |
17 | fx.elem.style[attr] = "rgb(" + [
18 | Math.max(Math.min( parseInt((fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0]), 255), 0),
19 | Math.max(Math.min( parseInt((fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1]), 255), 0),
20 | Math.max(Math.min( parseInt((fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2]), 255), 0)
21 | ].join(",") + ")";
22 | }
23 | });
24 |
25 | // Color Conversion functions from highlightFade
26 | // By Blair Mitchelmore
27 | // http://jquery.offput.ca/highlightFade/
28 |
29 | // Parse strings looking for color tuples [255,255,255]
30 | function getRGB(color) {
31 | var result;
32 |
33 | // Check if we're already dealing with an array of colors
34 | if ( color && color.constructor == Array && color.length == 3 )
35 | return color;
36 |
37 | // Look for rgb(num,num,num)
38 | if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color))
39 | return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])];
40 |
41 | // Look for rgb(num%,num%,num%)
42 | if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color))
43 | return [parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55];
44 |
45 | // Look for #a0b1c2
46 | if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color))
47 | return [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16)];
48 |
49 | // Look for #fff
50 | if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color))
51 | return [parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)];
52 |
53 | // Otherwise, we're most likely dealing with a named color
54 | return colors[jQuery.trim(color).toLowerCase()];
55 | }
56 |
57 | function getColor(elem, attr) {
58 | var color;
59 |
60 | do {
61 | color = jQuery.curCSS(elem, attr);
62 |
63 | // Keep going until we find an element that has color, or we hit the body
64 | if ( color != '' && color != 'transparent' || jQuery.nodeName(elem, "body") )
65 | break;
66 |
67 | attr = "backgroundColor";
68 | } while ( elem = elem.parentNode );
69 |
70 | return getRGB(color);
71 | };
72 |
73 | // Some named colors to work with
74 | // From Interface by Stefan Petre
75 | // http://interface.eyecon.ro/
76 |
77 | var colors = {
78 | aqua:[0,255,255],
79 | azure:[240,255,255],
80 | beige:[245,245,220],
81 | black:[0,0,0],
82 | blue:[0,0,255],
83 | brown:[165,42,42],
84 | cyan:[0,255,255],
85 | darkblue:[0,0,139],
86 | darkcyan:[0,139,139],
87 | darkgrey:[169,169,169],
88 | darkgreen:[0,100,0],
89 | darkkhaki:[189,183,107],
90 | darkmagenta:[139,0,139],
91 | darkolivegreen:[85,107,47],
92 | darkorange:[255,140,0],
93 | darkorchid:[153,50,204],
94 | darkred:[139,0,0],
95 | darksalmon:[233,150,122],
96 | darkviolet:[148,0,211],
97 | fuchsia:[255,0,255],
98 | gold:[255,215,0],
99 | green:[0,128,0],
100 | indigo:[75,0,130],
101 | khaki:[240,230,140],
102 | lightblue:[173,216,230],
103 | lightcyan:[224,255,255],
104 | lightgreen:[144,238,144],
105 | lightgrey:[211,211,211],
106 | lightpink:[255,182,193],
107 | lightyellow:[255,255,224],
108 | lime:[0,255,0],
109 | magenta:[255,0,255],
110 | maroon:[128,0,0],
111 | navy:[0,0,128],
112 | olive:[128,128,0],
113 | orange:[255,165,0],
114 | pink:[255,192,203],
115 | purple:[128,0,128],
116 | violet:[128,0,128],
117 | red:[255,0,0],
118 | silver:[192,192,192],
119 | white:[255,255,255],
120 | yellow:[255,255,0]
121 | };
122 |
123 | })(jQuery);
124 |
--------------------------------------------------------------------------------
/resources/public/javascript/register.js:
--------------------------------------------------------------------------------
1 | /*global jQuery, location */
2 | (function ($) {
3 |
4 | function switch_to_login_form() {
5 | $("#welcome form").attr("action", "login");
6 | $("#welcome h3").text("login");
7 | $("#welcome #password2").hide_password_field();
8 | $("#welcome #toggle_register_login").text("new here?").blur();
9 | $("#welcome #username").focus().select();
10 | }
11 |
12 | function switch_to_register_form() {
13 | $("#welcome form").attr("action", "register");
14 | $("#welcome h3").text("register");
15 | $("#welcome #password2").show_password_field();
16 | $("#welcome #toggle_register_login").text("already registered?").blur();
17 | }
18 |
19 | function toggle_register_login() {
20 | var current = $("#welcome form").attr("action");
21 | if (current === "register") {
22 | switch_to_login_form();
23 | } else {
24 | switch_to_register_form();
25 | }
26 | $("#welcome form").find(".validation_error, .double_error").remove();
27 | return false;
28 | }
29 |
30 | function username() {
31 | return $("#welcome #username").val();
32 | }
33 |
34 | function logged_in() {
35 | location.href = location.href.replace('?session=expired', '');
36 | }
37 |
38 | function show_mismatched_password_validation() {
39 | var error = $("these aren't equal (they should be)
").insertAfter("#password").hide().fadeIn(500);
40 | if ($.browser.opera) {
41 | error.css("margin-top", "5px");
42 | }
43 | $("#password, #password2").keyup(function () {
44 | var p1 = $("#password").val(), p2 = $("#password2").val();
45 | if (p1 === p2) {
46 | error.fadeOut(500, function () {
47 | error.remove();
48 | });
49 | $("#password, #password2").unbind("keyup");
50 | }
51 | });
52 | }
53 |
54 | function current_action() {
55 | return $("#welcome h3").text();
56 | }
57 |
58 | var wrong_password_before = false;
59 |
60 | function error_when_logging_in(json) {
61 | switch (json.result) {
62 | case "unavailable_username":
63 | $("#username").show_validation_error("sorry, that username is unavailable").focus().select();
64 | return true;
65 | case "unknown_username":
66 | $("#username").show_validation_error("sorry, this username is new to me").focus().select();
67 | return true;
68 | case "missing_username":
69 | $("#username").show_validation_error("you need a username to " + current_action()).focus();
70 | return true;
71 | case "wrong_password":
72 | if (wrong_password_before) {
73 | $("#password").show_validation_error("that's not right either - need help? ").focus().select();
74 | } else {
75 | wrong_password_before = true;
76 | $("#password").show_validation_error("sorry, that's not the right password").focus().select();
77 | }
78 | return true;
79 | case "missing_password":
80 | $("#password").show_validation_error("you need a password to " + current_action()).prev().trigger("focus").next().focus();
81 | return true;
82 | case "mismatched_passwords":
83 | show_mismatched_password_validation();
84 | return true;
85 | default:
86 | return false;
87 | }
88 | }
89 |
90 | function submit_register_login_form(event) {
91 | var form = $(this);
92 | event.preventDefault();
93 | $.postJSON("actions/" + form.attr("action"), form.serializeArray(), logged_in, error_when_logging_in);
94 | return false;
95 | }
96 |
97 | function should_show_login_form_as_default() {
98 | return $("#session_expired").exists() || username() !== "username";
99 | }
100 |
101 | function initialize() {
102 | $("#username").add_hint("username");
103 | $("#password").add_hint("password");
104 | $("#password2").add_hint("password again");
105 | $("#toggle_register_login").click(toggle_register_login);
106 | $("#welcome form").submit(submit_register_login_form);
107 | if (should_show_login_form_as_default()) {
108 | switch_to_login_form();
109 | }
110 | $("#submit").val("let's start").attr("disabled", "");
111 | }
112 |
113 | initialize();
114 |
115 | })(jQuery);
--------------------------------------------------------------------------------
/resources/public/javascript/sound_player.js:
--------------------------------------------------------------------------------
1 | /*global Audio, AC_FL_RunContent, window, document, navigator */
2 | var MT = MT || {};
3 |
4 | (function () {
5 |
6 | var dummy_swf = {
7 | playSound: function () {},
8 | stopSound: function () {}
9 | };
10 |
11 | var get_mp3player_swf = function () {
12 | var player;
13 | if (navigator.appName.indexOf("Microsoft") !== -1) {
14 | player = window.player;
15 | } else {
16 | player = document.player;
17 | }
18 | return player || dummy_swf;
19 | };
20 |
21 | var expand_audio = function (audio) {
22 | return {
23 | with_loop: function (other) {
24 | audio.stop = function () {
25 | this.currentTime = 0;
26 | this.pause();
27 | };
28 | audio.addEventListener("ended", function () {
29 | this.stop();
30 | other.play();
31 | }, false);
32 | }
33 | };
34 | };
35 |
36 | MT.sound_player = {
37 |
38 | audio_tag_supported: function () {
39 | return window.Audio && !!(new Audio().canPlayType);
40 | },
41 |
42 | create: function () {
43 | return this.audio_tag_supported() ? this.create_audio_player() : this.create_flash_player();
44 | },
45 |
46 | create_flash_player: function () {
47 | var create_flash_object = AC_FL_RunContent; // silence jslint
48 | create_flash_object('codebase', 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,28,0', 'name', 'player', 'width', '1', 'height', '1', 'title', 'mp3player', 'id', 'player', 'src', 'mp3player', 'quality', 'high', 'pluginspage', 'http://www.adobe.com/shockwave/download/download.cgi?P1_Prod_Version=ShockwaveFlash', 'movie', 'mp3player');
49 | return {
50 | type: "flash",
51 | swf: get_mp3player_swf(),
52 | load_audio: function () {
53 | },
54 | play_alarm: function () {
55 | this.swf.playSound("sounds/alarm.mp3");
56 | },
57 | stop_alarm: function () {
58 | this.swf.stopSound();
59 | },
60 | supports_ticking: false
61 | };
62 | },
63 |
64 | get_audio_elements: function () {
65 | return {
66 | alarm: document.getElementById("alarm_audio"),
67 | ticking_1: document.getElementById("ticking_audio_1"),
68 | ticking_2: document.getElementById("ticking_audio_2")
69 | };
70 | },
71 |
72 | create_audio_player: function () {
73 | var elements = this.get_audio_elements();
74 |
75 | expand_audio(elements.ticking_1).with_loop(elements.ticking_2);
76 | expand_audio(elements.ticking_2).with_loop(elements.ticking_1);
77 |
78 | return {
79 | type: "audio",
80 | load_audio: function () {
81 | if (!elements.alarm.readyState) {
82 | elements.alarm.load();
83 | }
84 | if (!elements.ticking_1.readyState) {
85 | elements.ticking_1.load();
86 | }
87 | if (!elements.ticking_2.readyState) {
88 | elements.ticking_2.load();
89 | }
90 | },
91 | play_alarm: function () {
92 | if (elements.alarm.readyState) {
93 | elements.alarm.currentTime = 0;
94 | elements.alarm.play();
95 | }
96 | },
97 | stop_alarm: function () {
98 | if (elements.alarm.readyState) {
99 | elements.alarm.pause();
100 | }
101 | },
102 | supports_ticking: true,
103 | start_ticking: function () {
104 | if (elements.ticking_1.readyState && elements.ticking_2.readyState) {
105 | elements.ticking_1.play();
106 | }
107 | },
108 | stop_ticking: function () {
109 | if (elements.ticking_1.readyState && elements.ticking_2.readyState) {
110 | elements.ticking_1.stop();
111 | elements.ticking_2.stop();
112 | }
113 | }
114 | };
115 | }
116 | };
117 | }());
--------------------------------------------------------------------------------
/resources/public/javascript/recovery.js:
--------------------------------------------------------------------------------
1 | /*global jQuery */
2 |
3 | var MT = this.MT || {};
4 |
5 | (function ($) {
6 |
7 | var invalidWord = function (s) {
8 | s = $.trim(s).toLowerCase();
9 | if (s.toLocaleLowerCase) { s = s.toLocaleLowerCase(); }
10 |
11 | if (s.length === 0) { return "we need all four words for the test"; }
12 | if (s.length === 1) { return "that's too short for our purposes"; }
13 | if (MT.commonWords[s]) { return "sorry, but this word is too common"; }
14 | };
15 |
16 | var validateWord = function (input) {
17 | var msg = invalidWord(input.value);
18 | if (msg) {
19 | $(input).show_validation_error(msg).focus();
20 | return false;
21 | }
22 | return true;
23 | };
24 |
25 | $("#fields .word").blur(function () {
26 | validateWord(this);
27 | });
28 |
29 | var validateUsername = function (input) {
30 | if ($.trim(input.value).length === 0) {
31 | $(input).show_validation_error("your old username goes here, okay?").focus();
32 | return false;
33 | }
34 | return true;
35 | };
36 |
37 | var byId = function (id) { return document.getElementById(id); };
38 |
39 | var bigSuccess = function (json) {
40 | window.location = json.url;
41 | };
42 |
43 | var failure = function (json) {
44 | switch (json.result) {
45 | case "already_logged_in": window.location = "/"; return true;
46 | case "unknown_username": return $("#username").show_validation_error("sorry, this username is new to me").focus().select();
47 | case "missing_username": return $("#username").show_validation_error("you need a username to do this").focus();
48 | case "missing_word1": return $("#word1").show_validation_error("we need all four words for the test").focus();
49 | case "missing_word2": return $("#word2").show_validation_error("we need all four words for the test").focus();
50 | case "missing_word3": return $("#word3").show_validation_error("we need all four words for the test").focus();
51 | case "missing_word4": return $("#word4").show_validation_error("we need all four words for the test").focus();
52 | case "duplicate_words": alert("We need four different words to make this work."); return true;
53 | case "too_common_words": alert("Some of the words you used are too common."); return true;
54 | case "not_enough_matches": alert("I'm sorry, but it doesn't look\nlike you've used all of these\nwords in your tomatoes."); return true;
55 | case "missed_word1": return $("#word1").show_validation_error("almost there, but this one isn't right").focus();
56 | case "missed_word2": return $("#word2").show_validation_error("almost there, but this one isn't right").focus();
57 | case "missed_word3": return $("#word3").show_validation_error("almost there, but this one isn't right").focus();
58 | case "missed_word4": return $("#word4").show_validation_error("almost there, but this one isn't right").focus();
59 | case "no_matches":
60 | alert("From what I can tell, you haven't used\nany of these words in your tomatoes.\n\nAre you sure this is the right username?");
61 | $("#username").show_validation_error("could you have used another name?").focus().select();
62 | return true;
63 | default: return false;
64 | }
65 | };
66 |
67 | var validateDuplicate = function (input, other) {
68 | var s1 = $.trim(input.value);
69 | var s2 = $.trim(other.value);
70 |
71 | if (s1 === s2) {
72 | $(input).show_validation_error("you already wrote this word").focus();
73 | return false;
74 | }
75 | return true;
76 | };
77 |
78 | $("#the-form").submit(function (e) {
79 | e.preventDefault();
80 |
81 | if (!validateUsername(byId("username"))) return false;
82 | if (!validateWord(byId("word1"))) return false;
83 | if (!validateWord(byId("word2"))) return false;
84 | if (!validateWord(byId("word3"))) return false;
85 | if (!validateWord(byId("word4"))) return false;
86 |
87 | if (!validateDuplicate(byId("word2"), byId("word1"))) return false;
88 | if (!validateDuplicate(byId("word3"), byId("word2"))) return false;
89 | if (!validateDuplicate(byId("word3"), byId("word1"))) return false;
90 | if (!validateDuplicate(byId("word4"), byId("word1"))) return false;
91 | if (!validateDuplicate(byId("word4"), byId("word2"))) return false;
92 | if (!validateDuplicate(byId("word4"), byId("word3"))) return false;
93 |
94 | $.postJSON("/actions/check-my-words", {
95 | username: byId("username").value,
96 | word1: byId("word1").value,
97 | word2: byId("word2").value,
98 | word3: byId("word3").value,
99 | word4: byId("word4").value
100 | }, bigSuccess, failure);
101 | });
102 |
103 | $("#submit").val("try these").attr("disabled", false);
104 |
105 | }(jQuery));
106 |
--------------------------------------------------------------------------------
/src/mytomatoes/layout.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.layout
2 | (:require [clojure.java.io :as io]
3 | [hiccup.core :refer [html]]
4 | [hiccup.page :as page]
5 | [mytomatoes.storage :refer [get-preferences]]
6 | [optimus.assets :as assets]
7 | [optimus.html]))
8 |
9 | (def bundles {"styles.css" ["/theme/css/reset.css"
10 | "/theme/css/master.css"]
11 | "lib.js" ["/javascript/external/jquery.js"
12 | "/javascript/external/jquery.color.js"
13 | "/javascript/external/jquery.url.js"
14 | "/javascript/external/shortcut.js"
15 | "/javascript/external/date.js"
16 | "/javascript/external/AC_RunActiveContent.js"
17 | "/javascript/library.js"
18 | "/javascript/ajax_service.js"]
19 | "login.js" ["/javascript/input_hints.js"
20 | "/javascript/register.js"]
21 | "home.js" ["/javascript/countdown.js"
22 | "/javascript/animation.js"
23 | "/javascript/sound_player.js"
24 | "/javascript/preferences.js"
25 | "/javascript/index.js"
26 | "/javascript/bootstrap.js"]
27 | "recovery.js" ["/javascript/recovery.js"]
28 | "change-password.js" ["/javascript/input_hints.js"
29 | "/javascript/change-password.js"]})
30 |
31 | (defn get-assets []
32 | (concat
33 | (assets/load-bundles "public" bundles)
34 | (assets/load-assets "public" ["/favicon.ico"
35 | "/mp3player.swf"
36 | "/theme/images/error.gif"
37 | #"/sounds/.+\.(mp3|ogg|wav)"])))
38 |
39 | (def banner
40 | #_{:id "donation-drive-2017"
41 | :contents (html [:p "Is mytomatoes helping you? I'm raising money to keep it running. "
42 | [:a {:href "https://www.gofundme.com/keep-mytomatoescom-up-and-running"
43 | :id "click-donate"
44 | :target "_blank"} "Want to help out?"]])}
45 | #_{:id "donations-done-2017"
46 | :contents (html [:p "One more year of mytomatoes has been secured by "
47 | [:a {:target "_blank" :href "https://www.gofundme.com/keep-mytomatoescom-up-and-running"}
48 | "49 wonderful donors"]
49 | " - thank you so much!"])}
50 | nil)
51 |
52 | (defn hide-banner [request]
53 | (when-let [account-id (:account-id (:session request))]
54 | ((keyword (str "hide-banner-" (:id banner)))
55 | (get-preferences (:db request) account-id))))
56 |
57 | (defn with-layout [request page]
58 | {:headers {"Content-Type" "text/html; charset=utf-8"}
59 | :status (:status page 200)
60 | :body (page/html5
61 | [:head
62 | [:meta {:charset "utf-8"}]
63 | [:title "mytomatoes.com"]
64 | (optimus.html/link-to-css-bundles request ["styles.css"])]
65 | [:body
66 | [:div {:id "main"}
67 | [:div {:id "header"}
68 | (when (:account-id (:session request))
69 | [:form {:id "logout" :method "POST", :action "/actions/logout"}
70 | [:input {:type "submit" :value "log out"}]])
71 | [:h1 "mytomatoes.com"
72 | [:div " simple pomodoro tracking"]]]
73 |
74 | (when (and banner (not (hide-banner request)))
75 | [:div {:id "banner"}
76 | (when (:account-id (:session request))
77 | [:a {:id "hide_banner" :href "#" :data-id (:id banner)}
78 | "hide"
79 | [:span " this banner"]])
80 | (:contents banner)])
81 |
82 | [:noscript
83 | [:style {:type "text/css"} "#states, #done, #welcome {display: none;}"]
84 | [:div {:id "noscript"}
85 | [:p
86 | "mytomatoes.com is a tool for use with the "
87 | [:a {:href "http://www.pomodorotechnique.com/"} "pomodoro technique"]
88 | " by "
89 | [:a {:href "http://francescocirillo.com/"} "Francesco Cirillo"]
90 | ". "
91 | [:em "It doesn't work without Javascript."]
92 | " Sorry."]]]
93 |
94 | (:body page)
95 |
96 | [:div {:id "push"}]]
97 | [:div {:id "footer"}
98 | [:a {:target "_blank" :href "http://www.pomodorotechnique.com/"}
99 | "read about the pomodoro technique"]
100 | (if (= 500 (:status page))
101 | " - and twitter your rage to "
102 | " - and twitter your feedback to ")
103 | [:a {:target "_blank" :href "http://twitter.com/mytomatoes"}
104 | "@mytomatoes"]
105 | (if (= 500 (:status page))
106 | " ><"
107 | " :-)")]]
108 | (optimus.html/link-to-js-bundles request (into ["lib.js"] (:script-bundles page)))
109 | (when (= :prod (:env request))
110 | [:script (slurp (io/resource "public/ga.js"))]))})
111 |
--------------------------------------------------------------------------------
/src/mytomatoes/word_stats.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.word-stats
2 | (:require [clojure.string :as str]
3 | [mytomatoes.storage :as st]))
4 |
5 | (defonce ^:private word-stats (atom {}))
6 |
7 | (defn split-into-words [ss]
8 | (->> ss
9 | (mapcat #(str/split % #"\W+"))
10 | (map str/lower-case)
11 | (remove #(< (count %) 2))
12 | (set)))
13 |
14 | (defn words-for-account [db id]
15 | (->> (st/get-tomatoes db id)
16 | (map :description)
17 | (split-into-words)))
18 |
19 | (defn- track-word [stats word account-id]
20 | (update-in stats [word] (fn [s] (if s (conj s account-id) #{account-id}))))
21 |
22 | (defn- track-words [stats words account-id]
23 | (reduce #(track-word %1 %2 account-id) stats words))
24 |
25 | (defn- populate-from-account! [db account-id]
26 | (swap! word-stats track-words (words-for-account db account-id) account-id)
27 | nil)
28 |
29 | (defn- populate! [db]
30 | (doseq [i (range 1 65341)]
31 | (when (= 0 (mod i 100)) (prn "And we're up to" i))
32 | (when (= 0 (mod i 1000))
33 | (prn "Writing out word-stats.edn")
34 | (spit "word-stats.edn" (prn-str @word-stats)))
35 | (populate-from-account! db i)))
36 |
37 | (comment ;; find common words
38 | (->> @word-stats
39 | (map (fn [[w s]] [w (count s)]))
40 | (remove (fn [[w c]] (< c 500)))
41 | (map first)))
42 |
43 | (def common-words
44 | #{"books" "total" "sources" "table" "abstract" "iv" "look" "map" "more"
45 | "sent" "sis" "lot" "ready" "essay" "parts" "lukemista" "going" "hw"
46 | "went" "lab" "desk" "org" "lis" "free" "50" "thesis" "got" "tomatoes"
47 | "34" "st" "slide" "organized" "loppuun" "created" "book" "development"
48 | "law" "see" "studies" "nearly" "cover" "begin" "mail" "min" "hk" "thing"
49 | "ii" "using" "outlined" "en" "la" "science" "progress" "downloaded"
50 | "list" "google" "are" "story" "didn" "right" "back" "calendar" "media"
51 | "started" "made" "word" "very" "next" "reading" "testing" "typed" "22"
52 | "teksti" "which" "26" "fix" "online" "call" "paper" "body" "of" "this"
53 | "decided" "model" "bio" "talk" "learning" "after" "discussion" "starting"
54 | "up" "al" "off" "food" "web" "mit" "copy" "find" "three" "tasks" "issue"
55 | "literature" "references" "bit" "28" "pg" "60" "typing" "figure" "14"
56 | "review" "two" "pages" "ll" "part" "checking" "etc" "printed" "cleaning"
57 | "ch" "para" "not" "ss" "hours" "put" "today" "readings" "group" "it"
58 | "over" "began" "class" "sections" "far" "teht" "emailed" "health" "job"
59 | "send" "el" "also" "by" "intro" "try" "sentences" "different" "plus"
60 | "design" "long" "structure" "something" "sorted" "reports" "fb" "is" "30"
61 | "21" "points" "method" "looked" "few" "like" "tables" "key" "interview"
62 | "process" "hard" "away" "form" "found" "methods" "adding" "project" "forms"
63 | "why" "researching" "doing" "second" "people" "focus" "good" "admin"
64 | "onto" "article" "together" "complete" "about" "80" "info" "website" "you"
65 | "guide" "33" "new" "management" "20" "valmis" "than" "chart" "lukua"
66 | "video" "artikkelin" "where" "looking" "completed" "small" "run" "stuff"
67 | "results" "just" "third" "email" "for" "post" "taking" "past" "read"
68 | "drafting" "files" "19" "revision" "should" "personal" "take" "searching"
69 | "tomorrow" "outline" "lecture" "fixed" "rest" "my" "continued" "17"
70 | "figured" "words" "update" "assignment" "note" "prepared" "materials"
71 | "25" "setting" "conclusion" "quotes" "2011" "short" "again" "tein" "laundry"
72 | "sort" "corrections" "couple" "computer" "draft" "summary" "formatting"
73 | "better" "15" "sheet" "42" "feedback" "document" "rough" "most" "grading"
74 | "ideas" "business" "office" "ei" "writing" "schedule" "practice" "lunch"
75 | "can" "power" "pi" "section" "math" "need" "main" "did" "was" "coding"
76 | "luin" "100" "that" "reviewing" "if" "check" "same" "another" "edit"
77 | "application" "make" "18" "citations" "36" "blog" "go" "half" "quiz"
78 | "don" "had" "overview" "12" "general" "student" "researched" "what" "an"
79 | "minutes" "nothing" "previous" "13" "dissertation" "nyt" "music"
80 | "introduction" "even" "or" "editing" "think" "de" "27" "finish" "start"
81 | "moved" "case" "shit" "have" "articles" "study" "problems" "am" "text"
82 | "question" "making" "pp" "ppt" "so" "them" "conference" "24" "things"
83 | "1st" "whole" "wrote" "changes" "almost" "presentation" "met" "etsin" "on"
84 | "week" "reviewed" "35" "break" "2010" "paragraphs" "paragraph" "statement"
85 | "old" "filled" "content" "graded" "site" "but" "getting" "methodology"
86 | "facebook" "self" "time" "policy" "added" "internet" "moving" "when"
87 | "syllabus" "searched" "excel" "task" "be" "se" "fixing" "out" "3rd"
88 | "watched" "tried" "200" "dinner" "teaching" "studying" "non" "continue"
89 | "mostly" "38" "and" "edits" "order" "ja" "final" "theory" "reference"
90 | "planning" "do" "last" "history" "myself" "proposal" "move" "source" "down"
91 | "type" "home" "app" "phone" "working" "too" "one" "comments" "revisions"
92 | "between" "et" "39" "pre" "talked" "everything" "state" "work" "version"
93 | "finding" "planned" "big" "argument" "updating" "how" "help" "other" "from"
94 | "submitted" "mails" "outlining" "library" "mind" "chapters" "really" "lots"
95 | "tests" "37" "answered" "trying" "no" "print" "took" "cv" "called" "revise"
96 | "ate" "luku" "social" "viel" "updated" "plan" "11" "with" "around" "actually"
97 | "response" "add" "file" "now" "set" "some" "will" "information" "all"
98 | "clean" "lesson" "aloitin" "worked" "re" "45" "then" "room" "thought"
99 | "through" "notes" "emails" "system" "exam" "ok" "well" "report" "session"
100 | "printing" "32" "prep" "chapter" "issues" "sorting" "problem" "much" "vs"
101 | "diss" "beginning" "papers" "research" "day" "plans" "survey" "analysis"
102 | "letters" "page" "before" "only" "still" "putting" "training" "way" "course"
103 | "to" "bibliography" "cleaned" "examples" "write" "into" "cut" "thinking"
104 | "cards" "little" "written" "distracted" "16" "use" "search" "revising"
105 | "edited" "data" "background" "get" "know" "sen" "art" "10" "tomato"
106 | "kirjoitin" "organization" "questions" "organizing" "checked" "we"
107 | "finishing" "as" "code" "40" "english" "done" "powerpoint" "life" "end" "me"
108 | "31" "at" "docs" "point" "stats" "meeting" "doc" "letter" "lit" "left"
109 | "description" "finally" "the" "though" "journal" "ty" "test" "yesterday"
110 | "slides" "change" "school" "sample" "23" "first" "there" "homework" "in"
111 | "29" "finished" "revised" "topic" "2nd" "studied" "organize" "material" "idea"})
112 |
--------------------------------------------------------------------------------
/resources/public/javascript/external/shortcut.js:
--------------------------------------------------------------------------------
1 | /**
2 | * http://www.openjs.com/scripts/events/keyboard_shortcuts/
3 | * Version : 2.01.A
4 | * By Binny V A
5 | * License : BSD
6 | */
7 | shortcut = {
8 | 'all_shortcuts':{},//All the shortcuts are stored in this array
9 | 'add': function(shortcut_combination,callback,opt) {
10 | //Provide a set of default options
11 | var default_options = {
12 | 'type':'keydown',
13 | 'propagate':false,
14 | 'disable_in_input':true,
15 | 'target':document,
16 | 'keycode':false
17 | }
18 | if(!opt) opt = default_options;
19 | else {
20 | for(var dfo in default_options) {
21 | if(typeof opt[dfo] == 'undefined') opt[dfo] = default_options[dfo];
22 | }
23 | }
24 |
25 | var ele = opt.target
26 | if(typeof opt.target == 'string') ele = document.getElementById(opt.target);
27 | var ths = this;
28 | shortcut_combination = shortcut_combination.toLowerCase();
29 |
30 | //The function to be called at keypress
31 | var func = function(e) {
32 | e = e || window.event;
33 |
34 | if(opt['disable_in_input']) { //Don't enable shortcut keys in Input, Textarea fields
35 | var element;
36 | if(e.target) element=e.target;
37 | else if(e.srcElement) element=e.srcElement;
38 | if(element.nodeType==3) element=element.parentNode;
39 |
40 | if(element.tagName == 'INPUT' || element.tagName == 'TEXTAREA') return;
41 | }
42 |
43 | //Find Which key is pressed
44 | if (e.keyCode) code = e.keyCode;
45 | else if (e.which) code = e.which;
46 | var character = String.fromCharCode(code).toLowerCase();
47 |
48 | if(code == 188) character=","; //If the user presses , when the type is onkeydown
49 | if(code == 190) character="."; //If the user presses , when the type is onkeydown
50 |
51 | var keys = shortcut_combination.split("+");
52 | //Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked
53 | var kp = 0;
54 |
55 | //Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken
56 | var shift_nums = {
57 | "`":"~",
58 | "1":"!",
59 | "2":"@",
60 | "3":"#",
61 | "4":"$",
62 | "5":"%",
63 | "6":"^",
64 | "7":"&",
65 | "8":"*",
66 | "9":"(",
67 | "0":")",
68 | "-":"_",
69 | "=":"+",
70 | ";":":",
71 | "'":"\"",
72 | ",":"<",
73 | ".":">",
74 | "/":"?",
75 | "\\":"|"
76 | }
77 | //Special Keys - and their codes
78 | var special_keys = {
79 | 'esc':27,
80 | 'escape':27,
81 | 'tab':9,
82 | 'space':32,
83 | 'return':13,
84 | 'enter':13,
85 | 'backspace':8,
86 |
87 | 'scrolllock':145,
88 | 'scroll_lock':145,
89 | 'scroll':145,
90 | 'capslock':20,
91 | 'caps_lock':20,
92 | 'caps':20,
93 | 'numlock':144,
94 | 'num_lock':144,
95 | 'num':144,
96 |
97 | 'pause':19,
98 | 'break':19,
99 |
100 | 'insert':45,
101 | 'home':36,
102 | 'delete':46,
103 | 'end':35,
104 |
105 | 'pageup':33,
106 | 'page_up':33,
107 | 'pu':33,
108 |
109 | 'pagedown':34,
110 | 'page_down':34,
111 | 'pd':34,
112 |
113 | 'left':37,
114 | 'up':38,
115 | 'right':39,
116 | 'down':40,
117 |
118 | 'num0':96,
119 | 'num1':97,
120 | 'num2':98,
121 | 'num3':99,
122 | 'num4':100,
123 | 'num5':101,
124 | 'num6':102,
125 | 'num7':103,
126 | 'num8':104,
127 | 'num9':105,
128 |
129 | 'f1':112,
130 | 'f2':113,
131 | 'f3':114,
132 | 'f4':115,
133 | 'f5':116,
134 | 'f6':117,
135 | 'f7':118,
136 | 'f8':119,
137 | 'f9':120,
138 | 'f10':121,
139 | 'f11':122,
140 | 'f12':123
141 | }
142 |
143 | var modifiers = {
144 | shift: { wanted:false, pressed:false},
145 | ctrl : { wanted:false, pressed:false},
146 | alt : { wanted:false, pressed:false},
147 | meta : { wanted:false, pressed:false} //Meta is Mac specific
148 | };
149 |
150 | if(e.ctrlKey) modifiers.ctrl.pressed = true;
151 | if(e.shiftKey) modifiers.shift.pressed = true;
152 | if(e.altKey) modifiers.alt.pressed = true;
153 | if(e.metaKey) modifiers.meta.pressed = true;
154 |
155 | for(var i=0; k=keys[i],i 1) { //If it is a special key
172 | if(special_keys[k] == code) kp++;
173 |
174 | } else if(opt['keycode']) {
175 | if(opt['keycode'] == code) kp++;
176 |
177 | } else { //The special keys did not match
178 | if(character == k) kp++;
179 | else {
180 | if(shift_nums[character] && e.shiftKey) { //Stupid Shift key bug created by using lowercase
181 | character = shift_nums[character];
182 | if(character == k) kp++;
183 | }
184 | }
185 | }
186 | }
187 |
188 | if(kp == keys.length &&
189 | modifiers.ctrl.pressed == modifiers.ctrl.wanted &&
190 | modifiers.shift.pressed == modifiers.shift.wanted &&
191 | modifiers.alt.pressed == modifiers.alt.wanted &&
192 | modifiers.meta.pressed == modifiers.meta.wanted) {
193 | callback(e);
194 |
195 | if(!opt['propagate']) { //Stop the event
196 | //e.cancelBubble is supported by IE - this will kill the bubbling process.
197 | e.cancelBubble = true;
198 | e.returnValue = false;
199 |
200 | //e.stopPropagation works in Firefox.
201 | if (e.stopPropagation) {
202 | e.stopPropagation();
203 | e.preventDefault();
204 | }
205 | return false;
206 | }
207 | }
208 | }
209 | this.all_shortcuts[shortcut_combination] = {
210 | 'callback':func,
211 | 'target':ele,
212 | 'event': opt['type']
213 | };
214 | //Attach the function with the event
215 | if(ele.addEventListener) ele.addEventListener(opt['type'], func, false);
216 | else if(ele.attachEvent) ele.attachEvent('on'+opt['type'], func);
217 | else ele['on'+opt['type']] = func;
218 | },
219 |
220 | //Remove the shortcut - just specify the shortcut and I will remove the binding
221 | 'remove':function(shortcut_combination) {
222 | shortcut_combination = shortcut_combination.toLowerCase();
223 | var binding = this.all_shortcuts[shortcut_combination];
224 | delete(this.all_shortcuts[shortcut_combination])
225 | if(!binding) return;
226 | var type = binding['event'];
227 | var ele = binding['target'];
228 | var callback = binding['callback'];
229 |
230 | if(ele.detachEvent) ele.detachEvent('on'+type, callback);
231 | else if(ele.removeEventListener) ele.removeEventListener(type, callback, false);
232 | else ele['on'+type] = false;
233 | }
234 | };
--------------------------------------------------------------------------------
/resources/public/theme/css/master.css:
--------------------------------------------------------------------------------
1 | html {margin: 0;}
2 | html, body {height: 100%;}
3 | #main {min-height: 100%; height: auto !important; height: 100%; margin: 0 auto -1.5em;}
4 | #footer, #push {height: 1.5em;}
5 | #export,
6 | #footer {text-align: center; font-size: 10px;color: #999;}
7 | #export a,
8 | #footer a {color: #99b;}
9 |
10 | #banner { background: #ffd995; border-bottom:2px solid #bb9551; border-top:2px solid #bb9551; font-size: 12px; padding: 10px; margin-top: -2px; position: relative;}
11 | #banner p {width: 420px; margin: 0 auto;}
12 | #banner a {color:#997330;}
13 | #hide_banner {position: absolute; right: 20px; top: 24px;}
14 | @media (max-width: 800px) {
15 | #hide_banner span {display: none;}
16 | }
17 |
18 | .strong {font-weight: bold;}
19 | .mas {margin: 5px;}
20 |
21 | #export {padding: 20px 0;}
22 |
23 | body {font-family:Verdana;background:#eee;}
24 | em {font-style: italic;}
25 |
26 | #header {padding:4px; border-bottom:2px solid #ddd;}
27 | #header h1 {font-size:2.0em;text-align:center;padding:2px 75px 8px;background: url(../images/pomodoro.png) no-repeat left 50%;width: 230px; margin: 0 auto;}
28 | #header h1 div {font-size:0.5em;color: #bb9551;text-align: left;padding-left: 4px;}
29 | #logout {padding: 4px; position: absolute; }
30 | #logout input {border: none; background: none; color: #99b; text-decoration: underline; cursor: pointer;}
31 |
32 | #error,
33 | #noscript,
34 | #welcome,
35 | #states {background-color:#fff;height:330px;text-align:center;border-bottom:2px solid #ddd;}
36 |
37 | #welcome.recovery {height: 550px;}
38 |
39 | #welcome p {padding-top:15px;width:420px;margin:0 auto;text-align:justify;}
40 | #welcome form {text-align:left;padding:10px 20px 20px 20px;border:2px solid #ddd;width:420px;margin:20px auto;}
41 | #welcome #toggle_register_login {float:right;margin-top:2px;}
42 | #welcome h3 {font-size:1.6em;padding-bottom:5px;}
43 | #welcome #fields input {width:405px;padding:5px;font-size:1.4em;margin-bottom:10px;}
44 | #welcome input.hint {color:#999;}
45 | #welcome #submit {font-size:1.4em;float:right;}
46 |
47 | #welcome #session_expired {font-size:1.3em; text-align: center; color: #bb9551; padding-top: 30px;}
48 |
49 | #welcome form {position: relative;}
50 | #welcome .validation_error {padding:10px 5px 10px 26px;width:187px;background:url(../images/validation.png) no-repeat left 50%;font-size:10px;position:absolute;right:-186px;margin-top:-44px;*margin-top:3px;}
51 | #welcome .double_error {padding:22px 5px 22px 26px;width:187px;background:url(../images/validation_double.png) no-repeat left 50%;font-size:10px;position:absolute;right:-186px;margin-top:-32px;*margin-top:9px;}
52 |
53 | #noscript p,
54 | #error p {padding-top:80px; padding-bottom: 20px; width: 450px; margin: 0 auto; text-align: justify;}
55 |
56 | #states #working,
57 | #states #stop_working,
58 | #states #enter_description,
59 | #states #on_a_break,
60 | #states #break_over {display:none;}
61 |
62 | #states #waiting #flash_message {padding-top:40px;font-size:1.6em;padding-bottom:20px;color:#fff;}
63 | #states #waiting a {display:block;width:270px;padding-top:22px;height:83px;margin:0 auto;font-size:1.8em;color:#000;text-decoration:none;background:url(../images/button.png) no-repeat left top;}
64 | #states #waiting a:hover {background-position:left -144px;}
65 |
66 | #states #working #time_left {font-family:Tahoma;font-size:13em;text-align:center;}
67 | #states #working #cancel a {color:#99c;}
68 |
69 | #states #stop_working #no_time_left {font-family:Tahoma;font-size:13em;text-align:center;}
70 | #states #stop_working #break {font-size:1.6em;}
71 |
72 | #states #enter_description #congratulations {padding-top:80px;margin:0 auto 20px;font-size:1.4em;color:#bb9551;}
73 | #states #enter_description #description {margin-top:20px;font-size:1.6em;}
74 | #states #enter_description input {font-size:2.4em;padding:4px;}
75 | #states #enter_description #void {margin-top:10px;}
76 |
77 | #states #on_a_break #break_left {font-family:Tahoma;font-size:13em;text-align:center;color:#cc6;}
78 | #states #on_a_break #well_deserved {font-size:1.6em;}
79 | #states #on_a_break #longer_break {margin-top: 10px;}
80 | #states #on_a_break #longer_break a {color: #996;}
81 | #states #on_a_break .longer_break_closed {font-size: 11px;}
82 | #states #on_a_break .longer_break_closed span {display: none;}
83 | #states #on_a_break .longer_break_open {font-size: inherit;}
84 | #states #on_a_break .longer_break_open #toggle_longer_break {color: #000; cursor: text; text-decoration: none;}
85 | #states #on_a_break .longer_break_open span {display: inline;}
86 |
87 | #states #break_over #no_break_left {font-family:Tahoma;font-size:13em;text-align:center;}
88 | #states #break_over #back_to_work {font-size:1.6em;}
89 |
90 | #preferences_container {width:550px;margin:-2px auto 2px;position: relative;}
91 | #preferences {width: 550px; position: absolute; top: -28px; height: 28px; overflow: hidden; opacity: 0.2; background: #f4f4f4; -moz-border-radius: 23px 23px 0 0; -webkit-border-radius: 23px 23px 0 0;}
92 | #preferences:hover {opacity: 1;}
93 | #preferences.open {opacity: 1; top: -220px; height: 220px; background: #eee; border-bottom: 0;}
94 | #preferences h3 {color: #669; margin: 0; position: relative; text-align: center; height: 23px; padding-top: 5px; font-size: 15px; background: url(../images/preferences.png) no-repeat 39% 60%; cursor: pointer;}
95 | #preferences.open h3 {color: #000; background-color: #ddd; -moz-border-radius: 23px 23px 0 0; -webkit-border-radius: 23px 23px 0 0;}
96 | #preferences li {border-bottom: 1px dashed #ddd; padding: 20px 50px;}
97 | #preferences label {cursor: pointer;}
98 | #preferences input {margin-right: 5px;}
99 | #preferences .note {font-size: 10px; color: #999; margin: 4px 0 0 22px;}
100 | #preferences .note a {color: #99b;}
101 | #preferences .disabled label {text-decoration: line-through; color: #999;}
102 | #preferences .disabled .note {color: #000;}
103 | #preferences .disabled .note a {color: #00f;}
104 |
105 | .done {width:550px;margin:10px auto 0;}
106 | .done h3 {font-size:2.0em;padding:0 50px;}
107 | .done h3 span {font-size:11px;color:#999;}
108 | .done ul {margin:5px 0 15px;border-top:1px solid #ddd;}
109 | .done li {border-top:1px solid #fff;border-bottom:1px solid #ddd;padding:2px 50px 5px;background:#f4f4f4;}
110 | .done li span {color:#999;font-size:11px;padding-right:10px;}
111 | .done .european_clock .ameritime,
112 | .done .american_clock .eurotime {display: none;}
113 | #show_older_tomatoes {margin:0 50px;color:#99c;}
114 | #show_older_tomatoes:hover {color:#66c;}
115 |
116 | #years {width:550px;margin:10px auto 0;}
117 | #years h3 {font-size:2.0em;padding:0 50px;}
118 | #years .year-count {font-size:11px;color:#999;}
119 | #years .show-year {color: #99b; cursor: pointer; text-decoration: underline;}
120 | #years .showing-year {background: #e0e0e0;}
121 |
122 | #audio {display: none;}
123 |
124 | #tutorial {padding-bottom: 20px;}
125 | #tutorial h3 {text-align:center;font-size:1.4em;margin:20px 0 5px;}
126 | #tutorial ul {margin:0 auto;text-align:center;*margin-left:5px;}
127 | #tutorial li {display:-moz-inline-stack;display:inline-block;width:110px;height:130px;vertical-align:top;font-size:11px;border:1px solid #999;background:#fff;text-align:left;zoom:1;*display:inline;*margin-right: 5px;}
128 | #tutorial p {padding:7px 10px;}
129 | #tutorial span {display:block;background:#f4f4f4 url(../images/tutorial.png) no-repeat left top;width:100px;height:65px;margin:5px 5px 0 5px;}
130 | #tutorial #waiting_tutorial span {background-position:0 0;}
131 | #tutorial #working_tutorial span {background-position:-100px 0;}
132 | #tutorial #stop_working_tutorial span {background-position:-200px 0;}
133 | #tutorial #enter_description_tutorial span {background-position:-300px 0;}
134 | #tutorial #on_a_break_tutorial span {background-position:-400px 0;}
135 | #tutorial #break_over_tutorial span {background-position:-500px 0;}
136 |
137 | #donate { display: none; padding: 20px; text-align: center; background: #ffc; border-bottom: 2px solid #ccc;}
138 | #close-donate {float: right; margin-left: 10px;}
139 |
--------------------------------------------------------------------------------
/src/mytomatoes/actions.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.actions
2 | (:require [clojure.set :as set]
3 | [clojure.string :as str]
4 | [mytomatoes.account :refer [password-matches?]]
5 | [mytomatoes.login :refer [remember! generate-auth-token]]
6 | [mytomatoes.storage :as s]
7 | [mytomatoes.util :refer [result]]
8 | [mytomatoes.word-stats :as ws]
9 | [taoensso.timbre :as timbre]))
10 | (timbre/refer-timbre)
11 |
12 | (defn blank? [s]
13 | (or (not s)
14 | (not (seq s))
15 | (not (seq (str/trim s)))))
16 |
17 | (defn register [{:keys [db params]}]
18 | (let [username (get params "username")
19 | password (get params "password")
20 | password2 (get params "password2")
21 | remember? (get params "remember")]
22 | (cond
23 | (blank? username) (result "missing_username")
24 | (= username "username") (result "missing_username")
25 | (blank? password) (result "missing_password")
26 | (s/account-exists? db username) (result "unavailable_username")
27 | (not= password password2) (result "mismatched_passwords")
28 | :else
29 | (let [account-id (s/create-account! db (str/trim username) password)]
30 | (info "Created account" username "with id" account-id)
31 | (-> (result "ok")
32 | (assoc :session {:account-id account-id})
33 | (cond-> remember? (remember! db account-id)))))))
34 |
35 | (defn login [{:keys [db params]}]
36 | (let [username (get params "username")
37 | password (get params "password")
38 | remember? (get params "remember")]
39 | (cond
40 | (blank? username) (result "missing_username")
41 | (= username "username") (result "missing_username")
42 | (blank? password) (result "missing_password")
43 | :else
44 | (let [account (s/get-account db (str/trim username))]
45 | (cond
46 | (nil? account) (result "unknown_username")
47 | (not (password-matches? account password))
48 | (do
49 | (info "Login attempt for" username "with id" (:id account) "with wrong password.")
50 | (result "wrong_password"))
51 | :else
52 | (do
53 | (info "Logged in" username "with id" (:id account) "using password.")
54 | (-> (result "ok")
55 | (assoc :session {:account-id (:id account)})
56 | (cond-> remember? (remember! db (:id account))))))))))
57 |
58 | (defn keep-session-alive []
59 | (result "ok"))
60 |
61 | (defn logout []
62 | {:session {}
63 | :cookies {"mytomatoes_remember" {:value "" :path "/"}}
64 | :status 302
65 | :headers {"Location" "/"}})
66 |
67 | (defn set-preference [{:keys [db params session]}]
68 | (let [name (get params "name")
69 | value (get params "value" "y")
70 | account-id (:account-id session)]
71 | (s/set-preference! {:account_id account-id
72 | :name name
73 | :value value} db)
74 | (info "Set preference" name "=" value "for" account-id)
75 | (result "ok")))
76 |
77 | (defn complete-tomato [{:keys [db params session]}]
78 | (let [account-id (:account-id session)
79 | start-time (get params "start_time")
80 | end-time (get params "end_time")
81 | description (get params "description")]
82 | (s/insert-complete-tomato! {:account_id account-id
83 | :description description
84 | :local_end end-time
85 | :local_start start-time}
86 | db)
87 | (info "Complete tomato for" account-id ":" description)
88 | (result "ok")))
89 |
90 | (defn check-my-words [{:keys [db params]}]
91 | (let [username (get params "username")
92 | word1 ^String (get params "word1")
93 | word2 ^String (get params "word2")
94 | word3 ^String (get params "word3")
95 | word4 ^String (get params "word4")
96 | proposed-words (into #{} (map str/lower-case [word1 word2 word3 word4]))]
97 | (cond
98 | (blank? username) (result "missing_username")
99 | (blank? word1) (result "missing_word1")
100 | (blank? word2) (result "missing_word2")
101 | (blank? word3) (result "missing_word3")
102 | (blank? word4) (result "missing_word4")
103 | (> 4 (count proposed-words)) (result "duplicate_words")
104 | (> 4 (count (set/difference proposed-words ws/common-words))) (do
105 | (warn "Attempt at using common words in recovery, " username ":" (set/intersection proposed-words ws/common-words))
106 | (result "too_common_words"))
107 | :else
108 | (let [account (s/get-account db (str/trim username))]
109 | (if (nil? account)
110 | (result "unknown_username")
111 | (let [proposed-words (ws/split-into-words proposed-words) ;; in case they think "its-hyphenated" is one word, but we think it's two
112 | actual-words (ws/words-for-account db (:id account))
113 | num-matches (count (set/intersection
114 | actual-words
115 | proposed-words))
116 | num-proposed (count proposed-words)]
117 | (cond
118 | (= num-proposed num-matches) (let [code (generate-auth-token)]
119 | (info "Successfull password word check for" username "with id" (:id account) ":" proposed-words)
120 | (s/add-remember-code! {:account_id (:id account)
121 | :code code}
122 | db)
123 | (result "ok" {:url (str "/change-password?code=" code)}))
124 | (= 0 num-matches) (do
125 | (info "Failed password word check with NO matching words for" username "with id" (:id account) ":" proposed-words)
126 | (result "no_matches"))
127 | (= (dec num-proposed) num-matches) (let [miss (first (set/difference proposed-words actual-words))]
128 | (info "Failed password check for" username "id" (:id account) "with one missing match:" miss)
129 | (cond
130 | ((ws/split-into-words [word1]) miss) (result "missed_word1")
131 | ((ws/split-into-words [word2]) miss) (result "missed_word2")
132 | ((ws/split-into-words [word3]) miss) (result "missed_word3")
133 | ((ws/split-into-words [word4]) miss) (result "missed_word4")))
134 | :else (do
135 | (info "Failed password word check with" num-matches "out of 4 matches for" username "with id" (:id account) ", wrong:" (set/difference proposed-words actual-words))
136 | (result "not_enough_matches")))))))))
137 |
138 | (defn change-password [{:keys [db params]}]
139 | (let [code (get params "code")
140 | password (get params "password")
141 | password2 (get params "password2")]
142 | (cond
143 | (blank? code) (do (info "Attempt to change password without a code.")
144 | (result "wrong_code"))
145 | (blank? password) (result "missing_password")
146 | (not= password password2) (result "mismatched_passwords")
147 | :else
148 | (if-let [account-id (s/get-account-id-by-remember-code db code)]
149 | (do
150 | (s/change-password! db account-id password)
151 | (s/remove-remember-code! {:code code} db)
152 | (info "Password changed for account" account-id)
153 | (-> (result "ok")
154 | (assoc :session {:account-id account-id})
155 | (remember! db account-id)))
156 | (do
157 | (info "Attempt to change password with invalid code: " code)
158 | (result "wrong_code"))))))
159 |
--------------------------------------------------------------------------------
/src/mytomatoes/pages/home.clj:
--------------------------------------------------------------------------------
1 | (ns mytomatoes.pages.home
2 | (:require [clj-time.core :as time]
3 | [clj-time.format :refer [formatter unparse]]
4 | [hiccup.core :refer [html]]
5 | [inflections.core :refer [plural]]
6 | [mytomatoes.layout :refer [with-layout]]
7 | [mytomatoes.storage :refer [get-tomatoes get-preferences]])
8 | (:import org.joda.time.DateTime))
9 |
10 | (defn- render-states []
11 | (html
12 | [:ul {:id "states"}
13 | [:li {:id "waiting"}
14 | [:div {:id "flash_message"} " "]
15 | [:a {:href "#"} "start tomato"]]
16 | [:li {:id "working"}
17 | [:div {:id "time_left"} "25 min "]
18 | [:div {:id "cancel"} [:a {:href "#"} "squash tomato"]]]
19 | [:li {:id "stop_working"}
20 | [:div {:id "no_time_left"} "0:00"]
21 | [:div {:id "break"} "time for a break"]]
22 | [:li {:id "enter_description"}
23 | [:div {:id "congratulations"} "congrats! " [:span "first"] " finished tomato today"]
24 | [:div {:id "description"} "what did you do?"]
25 | [:form [:input {:type "text" :maxlength "255"}]]
26 | [:div {:id "void"} "or " [:a {:href "#"} "squash tomato"]]]
27 | [:li {:id "on_a_break"}
28 | [:div {:id "break_left"} "5 min"]
29 | [:div {:id "well_deserved"} "a well deserved break"]
30 | [:div {:id "longer_break" :class "longer_break_closed"}
31 | [:a {:id "toggle_longer_break" :href "#"} "take a longer break"]
32 | [:span ": "
33 | [:a {:href "#"} "10"] " "
34 | [:a {:href "#"} "15"] " "
35 | [:a {:href "#"} "20"] " "
36 | [:a {:href "#"} "25"] " min"]]]
37 | [:li {:id "break_over"}
38 | [:div {:id "no_break_left"} "0:00"]
39 | [:div {:id "back_to_work"} "back to work!"]]]))
40 |
41 | (defn- render-preferences [{:keys [play-ticking use-american-clock show-notifications]}]
42 | (html
43 | [:div {:id "preferences_container"}
44 | [:div {:id "preferences"}
45 | [:h3 "preferences"]
46 | [:ul
47 | [:li {:id "ticking_preference"}
48 | [:label
49 | [:input {:type "checkbox" :name "play_ticking" :checked play-ticking}]
50 | " Play ticking sound when working on a tomato"]
51 | [:div {:class "note"} "Not a fan of the ticking? I recommend "
52 | [:a {:href "http://simplynoise.com" :target "_blank"} "simplynoise.com"] "!"]]
53 | [:li {:id "clock_preference"}
54 | [:label
55 | [:input {:type "checkbox" :name "use_american_clock" :checked use-american-clock}]
56 | " Use 12-hour clock"]]
57 | [:li {:id "notify_preference"}
58 | [:label
59 | [:input {:type "checkbox" :name "show_notifications" :checked show-notifications}
60 | " Show browser notifications"]]]]]]))
61 |
62 | (defn- render-tutorial []
63 | (html
64 | [:div {:id "tutorial"}
65 | [:h3 "how does this work?"]
66 | [:ul
67 | [:li {:id "waiting_tutorial"} [:div [:span] [:p "decide upon a task, and start the tomato"]]] " "
68 | [:li {:id "working_tutorial"} [:div [:span] [:p "work for 25 uninterrupted minutes"]]] " "
69 | [:li {:id "stop_working_tutorial"} [:div [:span] [:p "stop working when the timer rings"]]] " "
70 | [:li {:id "enter_description_tutorial"} [:div [:span] [:p "write what you did in a few words"]]] " "
71 | [:li {:id "on_a_break_tutorial"} [:div [:span] [:p "take a 5 minute break"]]] " "
72 | [:li {:id "break_over_tutorial"} [:div [:span] [:p "start a new tomato when the timer rings"]]]]]))
73 |
74 | (defn pluralize [count s]
75 | (str count (if (> count 1) (plural s) s)))
76 |
77 | (def eurotime (formatter "HH:mm"))
78 | (def ameritime (formatter "KK:mm a"))
79 |
80 | (defn- render-tomato [num i tomato]
81 | (html
82 | [:li
83 | [:span {:class "eurotime"}
84 | (unparse eurotime (:local-start tomato)) " - "
85 | (unparse eurotime (:local-end tomato))]
86 | [:span {:class "ameritime"}
87 | (unparse ameritime (:local-start tomato)) " - "
88 | (unparse ameritime (:local-end tomato))]
89 | " "
90 | (or (not-empty (:description tomato))
91 | (str "tomato #" (- num i) " finished"))]))
92 |
93 | (defn- render-day [[date tomatoes]]
94 | (let [num (count tomatoes)]
95 | (html
96 | [:h3 [:strong (str date)] " " [:span (pluralize num " finished tomato")]]
97 | [:ul
98 | (map-indexed (partial render-tomato num) tomatoes)])))
99 |
100 | (defn- render-completed-tomatoes [tomatoes prefs]
101 | (let [days (->> tomatoes
102 | (group-by :date)
103 | (sort-by first)
104 | (reverse))]
105 | (html
106 | [:div {:class (if (:use-american-clock prefs)
107 | "american_clock"
108 | "european_clock")}
109 | (map render-day days)])))
110 |
111 | (def csv-link
112 | "")
113 |
114 | (defn- render-audio []
115 | (html
116 | [:div {:id "audio"}
117 | [:audio {:id "alarm_audio" :autobuffer true :preload "auto"}
118 | [:source {:src "sounds/alarm.ogg" :type "audio/ogg;codecs=vorbis"}]
119 | [:source {:src "sounds/alarm.mp3" :type "audio/mpeg;codecs=mp3"}]
120 | [:source {:src "sounds/alarm.wav" :type "audio/x-wav;codecs=1"}]]
121 | [:audio {:id "ticking_audio_1" :autobuffer true :preload "auto"}
122 | [:source {:src "sounds/ticking.ogg" :type "audio/ogg;codecs=vorbis"}]
123 | [:source {:src "sounds/ticking.mp3" :type "audio/mpeg;codecs=mp3"}]
124 | [:source {:src "sounds/ticking.wav" :type "audio/x-wav;codecs=1"}]]
125 | [:audio {:id "ticking_audio_2" :autobuffer true :preload "auto"}
126 | [:source {:src "sounds/ticking.ogg" :type "audio/ogg;codecs=vorbis"}]
127 | [:source {:src "sounds/ticking.mp3" :type "audio/mpeg;codecs=mp3"}]
128 | [:source {:src "sounds/ticking.wav" :type "audio/x-wav;codecs=1"}]]]))
129 |
130 | (defn render-donation-banner [prefs tomatoes]
131 | (when (and (not (:hide-donation-2017 prefs))
132 | (not (:hide-banner-donation-drive-2017 prefs))
133 | (< 10 (count tomatoes)))
134 | (html [:div {:id "donate"}
135 | [:a {:href "#" :id "close-donate"} "hide"]
136 | "Is mytomatoes helping you? "
137 | [:a {:href "https://www.gofundme.com/keep-mytomatoescom-up-and-running"
138 | :id "click-donate"
139 | :target "_blank"} "Help keep the server running"]])))
140 |
141 | (defn get-year [tomato]
142 | (.getYear ^DateTime (:local-start tomato)))
143 |
144 | (defn render-year [[year count]]
145 | (html [:div {:class "year-holder"}
146 | [:h3 [:span {:class "show-year" :data-year year} year]
147 | " " [:span {:class "year-count"} count " finished tomatoes"]]]))
148 |
149 | (defn render-years [tomatoes]
150 | (when (seq tomatoes)
151 | (str ""
152 | (->> tomatoes
153 | (map get-year)
154 | frequencies
155 | (sort-by (comp - first))
156 | (map render-year)
157 | (apply str))
158 | "
")))
159 |
160 | (defn get-page [{:keys [db session] :as request}]
161 | (let [tomatoes (get-tomatoes db (:account-id session))
162 | prefs (get-preferences db (:account-id session))
163 | this-year (.getYear ^DateTime (time/now))
164 | this-year? #(= this-year (get-year %))]
165 | (with-layout request
166 | {:body
167 | (str (render-states)
168 | (render-preferences prefs)
169 | #_(render-donation-banner prefs tomatoes)
170 | (when-not (:hide-tutorial prefs) (render-tutorial))
171 | ""
172 | (render-completed-tomatoes (filter this-year? tomatoes) prefs)
173 | "
"
174 | (render-years (remove this-year? tomatoes))
175 | (when (< 5 (count tomatoes)) csv-link)
176 | (render-audio))
177 | :script-bundles ["home.js"]})))
178 |
179 | (defn completed-tomatoes-fragment [{:keys [db session]}]
180 | (let [tomatoes (get-tomatoes db (:account-id session))
181 | prefs (get-preferences db (:account-id session))
182 | this-year (.getYear ^DateTime (time/now))
183 | this-year? #(= this-year (get-year %))]
184 | (render-completed-tomatoes (filter this-year? tomatoes) prefs)))
185 |
186 | (defn yearly-tomatoes [{:keys [db session]} year]
187 | (let [tomatoes (get-tomatoes db (:account-id session))
188 | prefs (get-preferences db (:account-id session))
189 | same-year? #(= year (get-year %))]
190 | (render-completed-tomatoes (filter same-year? tomatoes) prefs)))
191 |
--------------------------------------------------------------------------------
/resources/public/javascript/external/AC_RunActiveContent.js:
--------------------------------------------------------------------------------
1 | //v1.7
2 | // Flash Player Version Detection
3 | // Detect Client Browser type
4 | // Copyright 2005-2007 Adobe Systems Incorporated. All rights reserved.
5 | var isIE = (navigator.appVersion.indexOf("MSIE") != -1) ? true : false;
6 | var isWin = (navigator.appVersion.toLowerCase().indexOf("win") != -1) ? true : false;
7 | var isOpera = (navigator.userAgent.indexOf("Opera") != -1) ? true : false;
8 |
9 | function ControlVersion()
10 | {
11 | var version;
12 | var axo;
13 | var e;
14 |
15 | // NOTE : new ActiveXObject(strFoo) throws an exception if strFoo isn't in the registry
16 |
17 | try {
18 | // version will be set for 7.X or greater players
19 | axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");
20 | version = axo.GetVariable("$version");
21 | } catch (e) {
22 | }
23 |
24 | if (!version)
25 | {
26 | try {
27 | // version will be set for 6.X players only
28 | axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");
29 |
30 | // installed player is some revision of 6.0
31 | // GetVariable("$version") crashes for versions 6.0.22 through 6.0.29,
32 | // so we have to be careful.
33 |
34 | // default to the first public version
35 | version = "WIN 6,0,21,0";
36 |
37 | // throws if AllowScripAccess does not exist (introduced in 6.0r47)
38 | axo.AllowScriptAccess = "always";
39 |
40 | // safe to call for 6.0r47 or greater
41 | version = axo.GetVariable("$version");
42 |
43 | } catch (e) {
44 | }
45 | }
46 |
47 | if (!version)
48 | {
49 | try {
50 | // version will be set for 4.X or 5.X player
51 | axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.3");
52 | version = axo.GetVariable("$version");
53 | } catch (e) {
54 | }
55 | }
56 |
57 | if (!version)
58 | {
59 | try {
60 | // version will be set for 3.X player
61 | axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.3");
62 | version = "WIN 3,0,18,0";
63 | } catch (e) {
64 | }
65 | }
66 |
67 | if (!version)
68 | {
69 | try {
70 | // version will be set for 2.X player
71 | axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
72 | version = "WIN 2,0,0,11";
73 | } catch (e) {
74 | version = -1;
75 | }
76 | }
77 |
78 | return version;
79 | }
80 |
81 | // JavaScript helper required to detect Flash Player PlugIn version information
82 | function GetSwfVer(){
83 | // NS/Opera version >= 3 check for Flash plugin in plugin array
84 | var flashVer = -1;
85 |
86 | if (navigator.plugins != null && navigator.plugins.length > 0) {
87 | if (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]) {
88 | var swVer2 = navigator.plugins["Shockwave Flash 2.0"] ? " 2.0" : "";
89 | var flashDescription = navigator.plugins["Shockwave Flash" + swVer2].description;
90 | var descArray = flashDescription.split(" ");
91 | var tempArrayMajor = descArray[2].split(".");
92 | var versionMajor = tempArrayMajor[0];
93 | var versionMinor = tempArrayMajor[1];
94 | var versionRevision = descArray[3];
95 | if (versionRevision == "") {
96 | versionRevision = descArray[4];
97 | }
98 | if (versionRevision[0] == "d") {
99 | versionRevision = versionRevision.substring(1);
100 | } else if (versionRevision[0] == "r") {
101 | versionRevision = versionRevision.substring(1);
102 | if (versionRevision.indexOf("d") > 0) {
103 | versionRevision = versionRevision.substring(0, versionRevision.indexOf("d"));
104 | }
105 | }
106 | var flashVer = versionMajor + "." + versionMinor + "." + versionRevision;
107 | }
108 | }
109 | // MSN/WebTV 2.6 supports Flash 4
110 | else if (navigator.userAgent.toLowerCase().indexOf("webtv/2.6") != -1) flashVer = 4;
111 | // WebTV 2.5 supports Flash 3
112 | else if (navigator.userAgent.toLowerCase().indexOf("webtv/2.5") != -1) flashVer = 3;
113 | // older WebTV supports Flash 2
114 | else if (navigator.userAgent.toLowerCase().indexOf("webtv") != -1) flashVer = 2;
115 | else if ( isIE && isWin && !isOpera ) {
116 | flashVer = ControlVersion();
117 | }
118 | return flashVer;
119 | }
120 |
121 | // When called with reqMajorVer, reqMinorVer, reqRevision returns true if that version or greater is available
122 | function DetectFlashVer(reqMajorVer, reqMinorVer, reqRevision)
123 | {
124 | versionStr = GetSwfVer();
125 | if (versionStr == -1 ) {
126 | return false;
127 | } else if (versionStr != 0) {
128 | if(isIE && isWin && !isOpera) {
129 | // Given "WIN 2,0,0,11"
130 | tempArray = versionStr.split(" "); // ["WIN", "2,0,0,11"]
131 | tempString = tempArray[1]; // "2,0,0,11"
132 | versionArray = tempString.split(","); // ['2', '0', '0', '11']
133 | } else {
134 | versionArray = versionStr.split(".");
135 | }
136 | var versionMajor = versionArray[0];
137 | var versionMinor = versionArray[1];
138 | var versionRevision = versionArray[2];
139 |
140 | // is the major.revision >= requested major.revision AND the minor version >= requested minor
141 | if (versionMajor > parseFloat(reqMajorVer)) {
142 | return true;
143 | } else if (versionMajor == parseFloat(reqMajorVer)) {
144 | if (versionMinor > parseFloat(reqMinorVer))
145 | return true;
146 | else if (versionMinor == parseFloat(reqMinorVer)) {
147 | if (versionRevision >= parseFloat(reqRevision))
148 | return true;
149 | }
150 | }
151 | return false;
152 | }
153 | }
154 |
155 | function AC_AddExtension(src, ext)
156 | {
157 | if (src.indexOf('?') != -1)
158 | return src.replace(/\?/, ext+'?');
159 | else
160 | return src + ext;
161 | }
162 |
163 | function AC_Generateobj(objAttrs, params, embedAttrs)
164 | {
165 | var str = '';
166 | if (isIE && isWin && !isOpera)
167 | {
168 | str += '';
174 | for (var i in params)
175 | {
176 | str += ' ';
177 | }
178 | str += ' ';
179 | }
180 | else
181 | {
182 | str += ' ';
188 | }
189 |
190 | document.write(str);
191 | }
192 |
193 | function AC_FL_RunContent(){
194 | var ret =
195 | AC_GetArgs
196 | ( arguments, ".swf", "movie", "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"
197 | , "application/x-shockwave-flash"
198 | );
199 | AC_Generateobj(ret.objAttrs, ret.params, ret.embedAttrs);
200 | }
201 |
202 | function AC_SW_RunContent(){
203 | var ret =
204 | AC_GetArgs
205 | ( arguments, ".dcr", "src", "clsid:166B1BCA-3F9C-11CF-8075-444553540000"
206 | , null
207 | );
208 | AC_Generateobj(ret.objAttrs, ret.params, ret.embedAttrs);
209 | }
210 |
211 | function AC_GetArgs(args, ext, srcParamName, classid, mimeType){
212 | var ret = new Object();
213 | ret.embedAttrs = new Object();
214 | ret.params = new Object();
215 | ret.objAttrs = new Object();
216 | for (var i=0; i < args.length; i=i+2){
217 | var currArg = args[i].toLowerCase();
218 |
219 | switch (currArg){
220 | case "classid":
221 | break;
222 | case "pluginspage":
223 | ret.embedAttrs[args[i]] = args[i+1];
224 | break;
225 | case "src":
226 | case "movie":
227 | args[i+1] = AC_AddExtension(args[i+1], ext);
228 | ret.embedAttrs["src"] = args[i+1];
229 | ret.params[srcParamName] = args[i+1];
230 | break;
231 | case "onafterupdate":
232 | case "onbeforeupdate":
233 | case "onblur":
234 | case "oncellchange":
235 | case "onclick":
236 | case "ondblClick":
237 | case "ondrag":
238 | case "ondragend":
239 | case "ondragenter":
240 | case "ondragleave":
241 | case "ondragover":
242 | case "ondrop":
243 | case "onfinish":
244 | case "onfocus":
245 | case "onhelp":
246 | case "onmousedown":
247 | case "onmouseup":
248 | case "onmouseover":
249 | case "onmousemove":
250 | case "onmouseout":
251 | case "onkeypress":
252 | case "onkeydown":
253 | case "onkeyup":
254 | case "onload":
255 | case "onlosecapture":
256 | case "onpropertychange":
257 | case "onreadystatechange":
258 | case "onrowsdelete":
259 | case "onrowenter":
260 | case "onrowexit":
261 | case "onrowsinserted":
262 | case "onstart":
263 | case "onscroll":
264 | case "onbeforeeditfocus":
265 | case "onactivate":
266 | case "onbeforedeactivate":
267 | case "ondeactivate":
268 | case "type":
269 | case "codebase":
270 | case "id":
271 | ret.objAttrs[args[i]] = args[i+1];
272 | break;
273 | case "width":
274 | case "height":
275 | case "align":
276 | case "vspace":
277 | case "hspace":
278 | case "class":
279 | case "title":
280 | case "accesskey":
281 | case "name":
282 | case "tabindex":
283 | ret.embedAttrs[args[i]] = ret.objAttrs[args[i]] = args[i+1];
284 | break;
285 | default:
286 | ret.embedAttrs[args[i]] = ret.params[args[i]] = args[i+1];
287 | }
288 | }
289 | ret.objAttrs["classid"] = classid;
290 | if (mimeType) ret.embedAttrs["type"] = mimeType;
291 | return ret;
292 | }
293 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
4 |
5 | 1. DEFINITIONS
6 |
7 | "Contribution" means:
8 |
9 | a) in the case of the initial Contributor, the initial code and
10 | documentation distributed under this Agreement, and
11 |
12 | b) in the case of each subsequent Contributor:
13 |
14 | i) changes to the Program, and
15 |
16 | ii) additions to the Program;
17 |
18 | where such changes and/or additions to the Program originate from and are
19 | distributed by that particular Contributor. A Contribution 'originates' from
20 | a Contributor if it was added to the Program by such Contributor itself or
21 | anyone acting on such Contributor's behalf. Contributions do not include
22 | additions to the Program which: (i) are separate modules of software
23 | distributed in conjunction with the Program under their own license
24 | agreement, and (ii) are not derivative works of the Program.
25 |
26 | "Contributor" means any person or entity that distributes the Program.
27 |
28 | "Licensed Patents" mean patent claims licensable by a Contributor which are
29 | necessarily infringed by the use or sale of its Contribution alone or when
30 | combined with the Program.
31 |
32 | "Program" means the Contributions distributed in accordance with this
33 | Agreement.
34 |
35 | "Recipient" means anyone who receives the Program under this Agreement,
36 | including all Contributors.
37 |
38 | 2. GRANT OF RIGHTS
39 |
40 | a) Subject to the terms of this Agreement, each Contributor hereby grants
41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to
42 | reproduce, prepare derivative works of, publicly display, publicly perform,
43 | distribute and sublicense the Contribution of such Contributor, if any, and
44 | such derivative works, in source code and object code form.
45 |
46 | b) Subject to the terms of this Agreement, each Contributor hereby grants
47 | Recipient a non-exclusive, worldwide, royalty-free patent license under
48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise
49 | transfer the Contribution of such Contributor, if any, in source code and
50 | object code form. This patent license shall apply to the combination of the
51 | Contribution and the Program if, at the time the Contribution is added by the
52 | Contributor, such addition of the Contribution causes such combination to be
53 | covered by the Licensed Patents. The patent license shall not apply to any
54 | other combinations which include the Contribution. No hardware per se is
55 | licensed hereunder.
56 |
57 | c) Recipient understands that although each Contributor grants the licenses
58 | to its Contributions set forth herein, no assurances are provided by any
59 | Contributor that the Program does not infringe the patent or other
60 | intellectual property rights of any other entity. Each Contributor disclaims
61 | any liability to Recipient for claims brought by any other entity based on
62 | infringement of intellectual property rights or otherwise. As a condition to
63 | exercising the rights and licenses granted hereunder, each Recipient hereby
64 | assumes sole responsibility to secure any other intellectual property rights
65 | needed, if any. For example, if a third party patent license is required to
66 | allow Recipient to distribute the Program, it is Recipient's responsibility
67 | to acquire that license before distributing the Program.
68 |
69 | d) Each Contributor represents that to its knowledge it has sufficient
70 | copyright rights in its Contribution, if any, to grant the copyright license
71 | set forth in this Agreement.
72 |
73 | 3. REQUIREMENTS
74 |
75 | A Contributor may choose to distribute the Program in object code form under
76 | its own license agreement, provided that:
77 |
78 | a) it complies with the terms and conditions of this Agreement; and
79 |
80 | b) its license agreement:
81 |
82 | i) effectively disclaims on behalf of all Contributors all warranties and
83 | conditions, express and implied, including warranties or conditions of title
84 | and non-infringement, and implied warranties or conditions of merchantability
85 | and fitness for a particular purpose;
86 |
87 | ii) effectively excludes on behalf of all Contributors all liability for
88 | damages, including direct, indirect, special, incidental and consequential
89 | damages, such as lost profits;
90 |
91 | iii) states that any provisions which differ from this Agreement are offered
92 | by that Contributor alone and not by any other party; and
93 |
94 | iv) states that source code for the Program is available from such
95 | Contributor, and informs licensees how to obtain it in a reasonable manner on
96 | or through a medium customarily used for software exchange.
97 |
98 | When the Program is made available in source code form:
99 |
100 | a) it must be made available under this Agreement; and
101 |
102 | b) a copy of this Agreement must be included with each copy of the Program.
103 |
104 | Contributors may not remove or alter any copyright notices contained within
105 | the Program.
106 |
107 | Each Contributor must identify itself as the originator of its Contribution,
108 | if any, in a manner that reasonably allows subsequent Recipients to identify
109 | the originator of the Contribution.
110 |
111 | 4. COMMERCIAL DISTRIBUTION
112 |
113 | Commercial distributors of software may accept certain responsibilities with
114 | respect to end users, business partners and the like. While this license is
115 | intended to facilitate the commercial use of the Program, the Contributor who
116 | includes the Program in a commercial product offering should do so in a
117 | manner which does not create potential liability for other Contributors.
118 | Therefore, if a Contributor includes the Program in a commercial product
119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend
120 | and indemnify every other Contributor ("Indemnified Contributor") against any
121 | losses, damages and costs (collectively "Losses") arising from claims,
122 | lawsuits and other legal actions brought by a third party against the
123 | Indemnified Contributor to the extent caused by the acts or omissions of such
124 | Commercial Contributor in connection with its distribution of the Program in
125 | a commercial product offering. The obligations in this section do not apply
126 | to any claims or Losses relating to any actual or alleged intellectual
127 | property infringement. In order to qualify, an Indemnified Contributor must:
128 | a) promptly notify the Commercial Contributor in writing of such claim, and
129 | b) allow the Commercial Contributor tocontrol, and cooperate with the
130 | Commercial Contributor in, the defense and any related settlement
131 | negotiations. The Indemnified Contributor may participate in any such claim
132 | at its own expense.
133 |
134 | For example, a Contributor might include the Program in a commercial product
135 | offering, Product X. That Contributor is then a Commercial Contributor. If
136 | that Commercial Contributor then makes performance claims, or offers
137 | warranties related to Product X, those performance claims and warranties are
138 | such Commercial Contributor's responsibility alone. Under this section, the
139 | Commercial Contributor would have to defend claims against the other
140 | Contributors related to those performance claims and warranties, and if a
141 | court requires any other Contributor to pay any damages as a result, the
142 | Commercial Contributor must pay those damages.
143 |
144 | 5. NO WARRANTY
145 |
146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
151 | appropriateness of using and distributing the Program and assumes all risks
152 | associated with its exercise of rights under this Agreement , including but
153 | not limited to the risks and costs of program errors, compliance with
154 | applicable laws, damage to or loss of data, programs or equipment, and
155 | unavailability or interruption of operations.
156 |
157 | 6. DISCLAIMER OF LIABILITY
158 |
159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
166 | OF SUCH DAMAGES.
167 |
168 | 7. GENERAL
169 |
170 | If any provision of this Agreement is invalid or unenforceable under
171 | applicable law, it shall not affect the validity or enforceability of the
172 | remainder of the terms of this Agreement, and without further action by the
173 | parties hereto, such provision shall be reformed to the minimum extent
174 | necessary to make such provision valid and enforceable.
175 |
176 | If Recipient institutes patent litigation against any entity (including a
177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself
178 | (excluding combinations of the Program with other software or hardware)
179 | infringes such Recipient's patent(s), then such Recipient's rights granted
180 | under Section 2(b) shall terminate as of the date such litigation is filed.
181 |
182 | All Recipient's rights under this Agreement shall terminate if it fails to
183 | comply with any of the material terms or conditions of this Agreement and
184 | does not cure such failure in a reasonable period of time after becoming
185 | aware of such noncompliance. If all Recipient's rights under this Agreement
186 | terminate, Recipient agrees to cease use and distribution of the Program as
187 | soon as reasonably practicable. However, Recipient's obligations under this
188 | Agreement and any licenses granted by Recipient relating to the Program shall
189 | continue and survive.
190 |
191 | Everyone is permitted to copy and distribute copies of this Agreement, but in
192 | order to avoid inconsistency the Agreement is copyrighted and may only be
193 | modified in the following manner. The Agreement Steward reserves the right to
194 | publish new versions (including revisions) of this Agreement from time to
195 | time. No one other than the Agreement Steward has the right to modify this
196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The
197 | Eclipse Foundation may assign the responsibility to serve as the Agreement
198 | Steward to a suitable separate entity. Each new version of the Agreement will
199 | be given a distinguishing version number. The Program (including
200 | Contributions) may always be distributed subject to the version of the
201 | Agreement under which it was received. In addition, after a new version of
202 | the Agreement is published, Contributor may elect to distribute the Program
203 | (including its Contributions) under the new version. Except as expressly
204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
205 | licenses to the intellectual property of any Contributor under this
206 | Agreement, whether expressly, by implication, estoppel or otherwise. All
207 | rights in the Program not expressly granted under this Agreement are
208 | reserved.
209 |
210 | This Agreement is governed by the laws of the State of New York and the
211 | intellectual property laws of the United States of America. No party to this
212 | Agreement will bring a legal action under this Agreement more than one year
213 | after the cause of action arose. Each party waives its rights to a jury trial
214 | in any resulting litigation.
--------------------------------------------------------------------------------
/resources/public/javascript/index.js:
--------------------------------------------------------------------------------
1 | /*global jQuery, MT, shortcut, location, setTimeout, confirm, document, window, Notification */
2 | (function ($) {
3 | var sound_player, enter_pressed_event, current_tomato, change_to_state = {}, current_state, current_tutorial_step;
4 |
5 | function minutes(min) {
6 | return min * (MT.debug ? 0.2 : 60);
7 | }
8 |
9 | function twentyfive_minutes() {
10 | return minutes(25);
11 | }
12 |
13 | function five_minutes() {
14 | return minutes(5);
15 | }
16 |
17 | function numbering(num) {
18 | if (num < 4) {
19 | return ["", "first", "second", "third"][num];
20 | } else {
21 | return num + "th";
22 | }
23 | }
24 |
25 | function show_longer_break_options() {
26 | $(this).blur();
27 | $("#longer_break").toggleClass("longer_break_closed longer_break_open");
28 | return false;
29 | }
30 |
31 | function change_to_this_break_length() {
32 | var $anchor = $(this), length = $anchor.text() * 1;
33 | $.cancel_countdown();
34 | $("#break_left").countdown(minutes(length), change_to_state.break_over);
35 | $("#longer_break").fadeOut();
36 | return false;
37 | }
38 |
39 | function flash_until_click(color, callback) {
40 | $("body").
41 | css("cursor", "pointer").
42 | one("click", function () {
43 | $("body").css("cursor", "auto");
44 | $("#states").stop().css("background-color", "#fff");
45 | callback();
46 | });
47 | $("#states").flash_background(color);
48 | }
49 |
50 | function click_body() {
51 | $("body").click().unbind("click");
52 | }
53 |
54 | function now() {
55 | return new Date();
56 | }
57 |
58 | function todays_tomatoes() {
59 | return $("#done #today").next();
60 | }
61 |
62 | function num_tomatoes_today() {
63 | return todays_tomatoes().find("li").length;
64 | }
65 |
66 | function escape(s) {
67 | return s.replace(/",
85 | "",
86 | this.european_interval(),
87 | " ",
88 | "",
89 | this.american_interval(),
90 | " ",
91 | this.description ? escape(this.description) : this.generic_description,
92 | ""
93 | ].join("");
94 | };
95 | me.parameters = function () {
96 | return {
97 | start_time: this.start_time.toTimestamp(),
98 | end_time: this.end_time.toTimestamp(),
99 | description: this.description
100 | };
101 | };
102 | me.save = function () {
103 | $.postJSON("/actions/complete_tomato", this.parameters());
104 | };
105 | return me;
106 | }
107 |
108 | function cancel_tomato() {
109 | if (confirm("Really squash tomato?")) {
110 | $.cancel_countdown();
111 | change_to_state.waiting();
112 | if (sound_player.supports_ticking) {
113 | sound_player.stop_ticking();
114 | }
115 | $("#flash_message").flash("tomato squashed");
116 | }
117 | return false;
118 | }
119 |
120 | function maybe_toggle_ticking() {
121 | if (current_state === "#working" && sound_player.supports_ticking) {
122 | if (this.checked) {
123 | sound_player.start_ticking();
124 | } else {
125 | sound_player.stop_ticking();
126 | }
127 | }
128 | }
129 |
130 | function toggle_clock_types() {
131 | if (this.checked) {
132 | $("div.european_clock").addClass("american_clock").removeClass("european_clock");
133 | } else {
134 | $("div.american_clock").addClass("european_clock").removeClass("american_clock");
135 | }
136 | }
137 |
138 | function toggle_notification_pref() {
139 | if (this.checked) {
140 | Notification.requestPermission();
141 | }
142 | }
143 |
144 | function disable_ticking_preference() {
145 | $("#ticking_preference input:checkbox").
146 | attr("disabled", true).
147 | closest("li").addClass("disabled").
148 | find(".note").html("Sorry, but ticking is not supported by this browser. Try Chrome ");
149 | }
150 |
151 | function disable_notification_preference() {
152 | $("#notify_preference input:checkbox").
153 | attr("disabled", true).
154 | closest("li").addClass("disabled").
155 | find(".note").html("Sorry, but notifications are not supported by this browser. Try Chrome ");
156 | }
157 |
158 | function update_todays_tomato_counter() {
159 | var num = num_tomatoes_today(), plural = num > 1 ? "es" : "";
160 | $("#today span").text(num + " finished tomato" + plural);
161 | }
162 |
163 | function add_todays_list_if_first() {
164 | if (! $("#done #today").exists()) {
165 | $("today ").prependTo("#done > div").data("date", MT.today());
166 | }
167 | }
168 |
169 | function complete_current_tomato() {
170 | current_tomato.description = $("#enter_description input").val();
171 | current_tomato.save();
172 | add_todays_list_if_first();
173 | $(current_tomato.html()).prependTo(todays_tomatoes()).highlight();
174 | update_todays_tomato_counter();
175 | change_to_state.on_a_break();
176 | return false;
177 | }
178 |
179 | function handle_enter_pressed() {
180 | if (enter_pressed_event) {
181 | enter_pressed_event();
182 | return false;
183 | }
184 | }
185 |
186 | function handle_enter_pressed_in_description_form() {
187 | if (current_state === "#enter_description") {
188 | complete_current_tomato();
189 | } else {
190 | handle_enter_pressed();
191 | }
192 | return false;
193 | }
194 |
195 | function keep_session_alive_while_working() {
196 | if (current_state === "#working" || current_state === "#stop_working") {
197 | $.postJSON("/actions/keep_session_alive", {x: "y"});
198 | setTimeout(keep_session_alive_while_working, 5 * 60 * 1000);
199 | }
200 | }
201 |
202 | function maybe_confirm_leaving_page() {
203 | switch (current_state) {
204 | case "#working":
205 | return "You are in the middle of a tomato.";
206 | case "#stop_working":
207 | case "#enter_description":
208 | return "Your finished tomato will only be saved if you enter a description.";
209 | }
210 | }
211 |
212 | function been_through_tutorial() {
213 | return current_tutorial_step === "#break_over";
214 | }
215 |
216 | function finish_tutorial() {
217 | $("#break_over_tutorial").fadeTo(1000, "0.3", function () {
218 | $("#tutorial").slideUp(2000, function () {
219 | $(this).remove();
220 | });
221 | });
222 | $.postJSON("/actions/set_preference", {name: "hide_tutorial"});
223 | }
224 |
225 | function next_tutorial_step() {
226 | $(current_tutorial_step + "_tutorial").fadeTo(1000, "0.3");
227 | current_tutorial_step = current_state;
228 | }
229 |
230 | function start_tutorial() {
231 | current_tutorial_step = "#waiting";
232 | next_tutorial_step("#working");
233 | }
234 |
235 | function start_or_finish_tutorial() {
236 | if (been_through_tutorial()) {
237 | finish_tutorial();
238 | } else {
239 | start_tutorial();
240 | }
241 | }
242 |
243 | function progress_tutorial() {
244 | if (current_state === "#waiting") {
245 | // do nothing
246 | } else if (current_state === "#working") {
247 | start_or_finish_tutorial();
248 | } else {
249 | next_tutorial_step();
250 | }
251 | }
252 |
253 | function change_state_to(state) {
254 | current_state = state;
255 | $("#states li").hide().filter(state).show();
256 | if ($("#tutorial").exists()) {
257 | progress_tutorial();
258 | }
259 | }
260 |
261 | function should_show_notification() {
262 | return window.Notification &&
263 | $("#notify_preference input:checkbox").is(":checked") &&
264 | Notification.permission === "granted";
265 | }
266 |
267 | /* states */
268 |
269 | change_to_state.working = function () {
270 | sound_player.load_audio();
271 | change_state_to("#working");
272 | current_tomato = tomato();
273 | $("#time_left").countdown(twentyfive_minutes(), change_to_state.stop_working);
274 | MT.make_sure_that_today_is_still_today();
275 | keep_session_alive_while_working();
276 | enter_pressed_event = false;
277 | if (sound_player.supports_ticking && $("#ticking_preference input:checkbox").is(":checked")) {
278 | sound_player.start_ticking();
279 | }
280 | return false;
281 | };
282 |
283 | change_to_state.stop_working = function () {
284 | change_state_to("#stop_working");
285 | if (sound_player.supports_ticking) {
286 | sound_player.stop_ticking();
287 | }
288 | sound_player.play_alarm();
289 | document.title = "break! - mytomatoes.com";
290 | if (should_show_notification()) {
291 | new Notification("Time for a break!");
292 | }
293 | flash_until_click("#ffcb6e", function () {
294 | sound_player.stop_alarm();
295 | current_tomato.end_time = now();
296 | document.title = "mytomatoes.com";
297 | change_to_state.enter_description();
298 | });
299 | enter_pressed_event = click_body;
300 | };
301 |
302 | change_to_state.enter_description = function () {
303 | change_state_to("#enter_description");
304 | $("#congratulations span").text(numbering(current_tomato.number));
305 | $("#enter_description input").focus().select();
306 | enter_pressed_event = complete_current_tomato;
307 | };
308 |
309 | change_to_state.on_a_break = function () {
310 | change_state_to("#on_a_break");
311 | $("#states").css("background-color", "#ffd");
312 | $("#longer_break").addClass("longer_break_closed").removeClass("longer_break_open").show();
313 | $("#break_left").countdown(five_minutes(), change_to_state.break_over);
314 | $("#donate").fadeIn(3000);
315 | if (window._gaq) { window._gaq.push(['_trackEvent', 'Donations', 'Displayed', "helping", null, true]);}
316 | MT.make_sure_that_today_is_still_today();
317 | enter_pressed_event = false;
318 | return false;
319 | };
320 |
321 | change_to_state.break_over = function () {
322 | change_state_to("#break_over");
323 | sound_player.play_alarm();
324 | if (should_show_notification()) {
325 | new Notification("Back to work!");
326 | }
327 | flash_until_click("#fff", function () {
328 | sound_player.stop_alarm();
329 | change_to_state.waiting();
330 | });
331 | $("#donate").remove();
332 | MT.make_sure_that_today_is_still_today();
333 | enter_pressed_event = click_body;
334 | };
335 |
336 | change_to_state.waiting = function () {
337 | change_state_to("#waiting");
338 | MT.make_sure_that_today_is_still_today();
339 | enter_pressed_event = change_to_state.working;
340 | };
341 |
342 | MT.initialize_index = function () {
343 | MT.fix_day_names();
344 | $("#waiting a").click(change_to_state.working);
345 | $("#cancel a, #void a").click(cancel_tomato);
346 | shortcut.add("enter", handle_enter_pressed);
347 | $("#enter_description form").submit(handle_enter_pressed_in_description_form);
348 | $("#toggle_longer_break").click(show_longer_break_options);
349 | $("#longer_break span a").click(change_to_this_break_length);
350 | $("#ticking_preference input:checkbox").click(maybe_toggle_ticking);
351 | $("#clock_preference input:checkbox").click(toggle_clock_types);
352 | $("#notify_preference input:checkbox").click(toggle_notification_pref);
353 | change_to_state.waiting();
354 | window.onbeforeunload = maybe_confirm_leaving_page;
355 | sound_player = MT.sound_player.create();
356 | if (!sound_player.supports_ticking) {
357 | disable_ticking_preference();
358 | }
359 | if (("Notification" in window)) {
360 | if ($("#notify_preference input:checkbox").is(":checked")) {
361 | Notification.requestPermission();
362 | }
363 | } else {
364 | disable_notification_preference();
365 | }
366 |
367 | $("#years .show-year").live("click", function () {
368 | $(this).
369 | removeClass("show-year").
370 | closest("h3").
371 | addClass("showing-year").
372 | closest(".year-holder").
373 | append("
").
374 | find(".done").
375 | load("views/yearly_tomatoes/" + this.getAttribute("data-year"), MT.fix_day_names);
376 | });
377 |
378 | $("#click-donate").click(function () {
379 | if (window._gaq) { window._gaq.push(['_trackEvent', 'Donations', 'Clicked', "helping", null, true]);}
380 | });
381 | $("#close-donate").click(function () {
382 | $.postJSON("/actions/set_preference", {name: "hide_donation_2017"});
383 | $("#donate").remove();
384 | if (window._gaq) { window._gaq.push(['_trackEvent', 'Donations', 'Closed', "helping", null, true]);}
385 | return false;
386 | });
387 |
388 | $("#hide_banner").click(function () {
389 | $.postJSON("/actions/set_preference", {name: "hide_banner_" + $(this).attr("data-id")});
390 | $("#banner").remove();
391 | });
392 | };
393 |
394 | })(jQuery);
395 |
--------------------------------------------------------------------------------
/resources/public/javascript/external/date.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Version: 1.0 Alpha-1
3 | * Build Date: 13-Nov-2007
4 | * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved.
5 | * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
6 | * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/
7 | */
8 | Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}};
9 | Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;idate)?1:(this=start.getTime()&&t<=end.getTime();};Date.prototype.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};Date.prototype.addSeconds=function(value){return this.addMilliseconds(value*1000);};Date.prototype.addMinutes=function(value){return this.addMilliseconds(value*60000);};Date.prototype.addHours=function(value){return this.addMilliseconds(value*3600000);};Date.prototype.addDays=function(value){return this.addMilliseconds(value*86400000);};Date.prototype.addWeeks=function(value){return this.addMilliseconds(value*604800000);};Date.prototype.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,this.getDaysInMonth()));return this;};Date.prototype.addYears=function(value){return this.addMonths(value*12);};Date.prototype.add=function(config){if(typeof config=="number"){this._orient=config;return this;}
14 | var x=config;if(x.millisecond||x.milliseconds){this.addMilliseconds(x.millisecond||x.milliseconds);}
15 | if(x.second||x.seconds){this.addSeconds(x.second||x.seconds);}
16 | if(x.minute||x.minutes){this.addMinutes(x.minute||x.minutes);}
17 | if(x.hour||x.hours){this.addHours(x.hour||x.hours);}
18 | if(x.month||x.months){this.addMonths(x.month||x.months);}
19 | if(x.year||x.years){this.addYears(x.year||x.years);}
20 | if(x.day||x.days){this.addDays(x.day||x.days);}
21 | return this;};Date._validate=function(value,min,max,name){if(typeof value!="number"){throw new TypeError(value+" is not a Number.");}else if(valuemax){throw new RangeError(value+" is not a valid value for "+name+".");}
22 | return true;};Date.validateMillisecond=function(n){return Date._validate(n,0,999,"milliseconds");};Date.validateSecond=function(n){return Date._validate(n,0,59,"seconds");};Date.validateMinute=function(n){return Date._validate(n,0,59,"minutes");};Date.validateHour=function(n){return Date._validate(n,0,23,"hours");};Date.validateDay=function(n,year,month){return Date._validate(n,1,Date.getDaysInMonth(year,month),"days");};Date.validateMonth=function(n){return Date._validate(n,0,11,"months");};Date.validateYear=function(n){return Date._validate(n,1,9999,"seconds");};Date.prototype.set=function(config){var x=config;if(!x.millisecond&&x.millisecond!==0){x.millisecond=-1;}
23 | if(!x.second&&x.second!==0){x.second=-1;}
24 | if(!x.minute&&x.minute!==0){x.minute=-1;}
25 | if(!x.hour&&x.hour!==0){x.hour=-1;}
26 | if(!x.day&&x.day!==0){x.day=-1;}
27 | if(!x.month&&x.month!==0){x.month=-1;}
28 | if(!x.year&&x.year!==0){x.year=-1;}
29 | if(x.millisecond!=-1&&Date.validateMillisecond(x.millisecond)){this.addMilliseconds(x.millisecond-this.getMilliseconds());}
30 | if(x.second!=-1&&Date.validateSecond(x.second)){this.addSeconds(x.second-this.getSeconds());}
31 | if(x.minute!=-1&&Date.validateMinute(x.minute)){this.addMinutes(x.minute-this.getMinutes());}
32 | if(x.hour!=-1&&Date.validateHour(x.hour)){this.addHours(x.hour-this.getHours());}
33 | if(x.month!==-1&&Date.validateMonth(x.month)){this.addMonths(x.month-this.getMonth());}
34 | if(x.year!=-1&&Date.validateYear(x.year)){this.addYears(x.year-this.getFullYear());}
35 | if(x.day!=-1&&Date.validateDay(x.day,this.getFullYear(),this.getMonth())){this.addDays(x.day-this.getDate());}
36 | if(x.timezone){this.setTimezone(x.timezone);}
37 | if(x.timezoneOffset){this.setTimezoneOffset(x.timezoneOffset);}
38 | return this;};Date.prototype.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};Date.prototype.isLeapYear=function(){var y=this.getFullYear();return(((y%4===0)&&(y%100!==0))||(y%400===0));};Date.prototype.isWeekday=function(){return!(this.is().sat()||this.is().sun());};Date.prototype.getDaysInMonth=function(){return Date.getDaysInMonth(this.getFullYear(),this.getMonth());};Date.prototype.moveToFirstDayOfMonth=function(){return this.set({day:1});};Date.prototype.moveToLastDayOfMonth=function(){return this.set({day:this.getDaysInMonth()});};Date.prototype.moveToDayOfWeek=function(day,orient){var diff=(day-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};Date.prototype.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};Date.prototype.getDayOfYear=function(){return Math.floor((this-new Date(this.getFullYear(),0,1))/86400000);};Date.prototype.getWeekOfYear=function(firstDayOfWeek){var y=this.getFullYear(),m=this.getMonth(),d=this.getDate();var dow=firstDayOfWeek||Date.CultureInfo.firstDayOfWeek;var offset=7+1-new Date(y,0,1).getDay();if(offset==8){offset=1;}
39 | var daynum=((Date.UTC(y,m,d,0,0,0)-Date.UTC(y,0,1,0,0,0))/86400000)+1;var w=Math.floor((daynum-offset+7)/7);if(w===dow){y--;var prevOffset=7+1-new Date(y,0,1).getDay();if(prevOffset==2||prevOffset==8){w=53;}else{w=52;}}
40 | return w;};Date.prototype.isDST=function(){return this.toString().match(/(E|C|M|P)(S|D)T/)[2]=="D";};Date.prototype.getTimezone=function(){return Date.getTimezoneAbbreviation(this.getUTCOffset,this.isDST());};Date.prototype.setTimezoneOffset=function(s){var here=this.getTimezoneOffset(),there=Number(s)*-6/10;this.addMinutes(there-here);return this;};Date.prototype.setTimezone=function(s){return this.setTimezoneOffset(Date.getTimezoneOffset(s));};Date.prototype.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r[0]+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};Date.prototype.getDayName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedDayNames[this.getDay()]:Date.CultureInfo.dayNames[this.getDay()];};Date.prototype.getMonthName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedMonthNames[this.getMonth()]:Date.CultureInfo.monthNames[this.getMonth()];};Date.prototype._toString=Date.prototype.toString;Date.prototype.toString=function(format){var self=this;var p=function p(s){return(s.toString().length==1)?"0"+s:s;};return format?format.replace(/dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?/g,function(format){switch(format){case"hh":return p(self.getHours()<13?self.getHours():(self.getHours()-12));case"h":return self.getHours()<13?self.getHours():(self.getHours()-12);case"HH":return p(self.getHours());case"H":return self.getHours();case"mm":return p(self.getMinutes());case"m":return self.getMinutes();case"ss":return p(self.getSeconds());case"s":return self.getSeconds();case"yyyy":return self.getFullYear();case"yy":return self.getFullYear().toString().substring(2,4);case"dddd":return self.getDayName();case"ddd":return self.getDayName(true);case"dd":return p(self.getDate());case"d":return self.getDate().toString();case"MMMM":return self.getMonthName();case"MMM":return self.getMonthName(true);case"MM":return p((self.getMonth()+1));case"M":return self.getMonth()+1;case"t":return self.getHours()<12?Date.CultureInfo.amDesignator.substring(0,1):Date.CultureInfo.pmDesignator.substring(0,1);case"tt":return self.getHours()<12?Date.CultureInfo.amDesignator:Date.CultureInfo.pmDesignator;case"zzz":case"zz":case"z":return"";}}):this._toString();};
41 | Date.now=function(){return new Date();};Date.today=function(){return Date.now().clearTime();};Date.prototype._orient=+1;Date.prototype.next=function(){this._orient=+1;return this;};Date.prototype.last=Date.prototype.prev=Date.prototype.previous=function(){this._orient=-1;return this;};Date.prototype._is=false;Date.prototype.is=function(){this._is=true;return this;};Number.prototype._dateElement="day";Number.prototype.fromNow=function(){var c={};c[this._dateElement]=this;return Date.now().add(c);};Number.prototype.ago=function(){var c={};c[this._dateElement]=this*-1;return Date.now().add(c);};(function(){var $D=Date.prototype,$N=Number.prototype;var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),de;var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;}
42 | return this.moveToDayOfWeek(n,this._orient);};};for(var i=0;i0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;}
70 | if(!last&&q[1].length===0){last=true;}
71 | if(!last){var qx=[];for(var j=0;j0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
73 | if(rx[1].length1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];}
80 | if(args){for(var i=0,px=args.shift();i2)?n:(n+(((n+2000)Date.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");}
84 | var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});}
85 | return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
86 | for(var i=0;i