├── .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 | "
Export tomatoes as csv
") 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 += ' '; 177 | } 178 | str += ''; 179 | } 180 | else 181 | { 182 | str += '", 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