From f376881cbc81691aeca7dd51c1f22fee44c8cbd0 Mon Sep 17 00:00:00 2001 From: gered Date: Sun, 28 Dec 2014 00:56:19 -0500 Subject: [PATCH] add todomvc app example --- examples/todomvc/.gitignore | 18 + examples/todomvc/create_db.sql | 21 + examples/todomvc/project.clj | 50 ++ examples/todomvc/resources/html/app.html | 19 + examples/todomvc/resources/public/todos.css | 558 ++++++++++++++++++ .../todomvc/resources/public/todosanim.css | 18 + examples/todomvc/src/todomvc/client.cljs | 105 ++++ examples/todomvc/src/todomvc/server.clj | 121 ++++ 8 files changed, 910 insertions(+) create mode 100644 examples/todomvc/.gitignore create mode 100644 examples/todomvc/create_db.sql create mode 100644 examples/todomvc/project.clj create mode 100644 examples/todomvc/resources/html/app.html create mode 100644 examples/todomvc/resources/public/todos.css create mode 100644 examples/todomvc/resources/public/todosanim.css create mode 100644 examples/todomvc/src/todomvc/client.cljs create mode 100644 examples/todomvc/src/todomvc/server.clj diff --git a/examples/todomvc/.gitignore b/examples/todomvc/.gitignore new file mode 100644 index 0000000..c7c1135 --- /dev/null +++ b/examples/todomvc/.gitignore @@ -0,0 +1,18 @@ +.DS_Store +/target +/classes +/checkouts +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port +.settings/ +.project +.classpath +.idea/ +*.iml +*.ipr +*.iws +/resources/public/cljs diff --git a/examples/todomvc/create_db.sql b/examples/todomvc/create_db.sql new file mode 100644 index 0000000..ec23eef --- /dev/null +++ b/examples/todomvc/create_db.sql @@ -0,0 +1,21 @@ +-- For PostgreSQL +-- run with psql. e.g. 'psql < create_db.sql' + +CREATE ROLE todomvc LOGIN PASSWORD 's3cr3t'; +CREATE DATABASE todomvc OWNER todomvc; + +-- assumes you're piping this script into psql ... +\c todomvc; + +CREATE TABLE todos +( + id SERIAL PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + done BOOLEAN DEFAULT FALSE NOT NULL +); +ALTER TABLE todos OWNER TO todomvc; + +INSERT INTO todos (title, done) VALUES ('Rename Cloact to Reagent', TRUE); +INSERT INTO todos (title, done) VALUES ('Add undo demo', TRUE); +INSERT INTO todos (title, done) VALUES ('Make all rendering async', TRUE); +INSERT INTO todos (title, done) VALUES ('Allow any arguments to component functions', TRUE); diff --git a/examples/todomvc/project.clj b/examples/todomvc/project.clj new file mode 100644 index 0000000..613d7a3 --- /dev/null +++ b/examples/todomvc/project.clj @@ -0,0 +1,50 @@ +(defproject todomvc "0.1.0-SNAPSHOT" + :dependencies [[org.clojure/clojure "1.6.0"] + [org.clojure/clojurescript "0.0-2371"] + [compojure "1.2.1"] + [ring "1.3.1"] + [ring/ring-defaults "0.1.3" :exclusions [javax.servlet/servlet-api]] + [net.thegeez/clj-browserchannel-jetty-adapter "0.0.6"] + [clj-browserchannel-messaging "0.0.4"] + [clj-pebble "0.2.0"] + [cljs-ajax "0.3.3"] + [reagent "0.5.0-alpha"] + [reagent-data-views "0.1.0-SNAPSHOT"] + [views "0.5.0"] + [org.clojure/java.jdbc "0.3.5"] + [org.postgresql/postgresql "9.2-1003-jdbc4"] + [honeysql "0.4.3"] + [environ "1.0.0"]] + + :plugins [[lein-cljsbuild "1.0.3"]] + + :main todomvc.server + + :cljsbuild {:builds + {:main + {:source-paths ["src"] + :compiler + {:preamble ["reagent/react.js"] + :output-to "resources/public/cljs/client.js" + :source-map "resources/public/cljs/client.js.map" + :output-dir "resources/public/cljs/client" + :optimizations :none + :pretty-print true}}}} + + :profiles {:dev {:env {:dev true}} + + :uberjar {:env {:dev false} + :hooks [leiningen.cljsbuild] + :cljsbuild + {:jar true + :builds + {:main + {:compiler + ^:replace + {:output-to "resources/public/cljs/client.js" + :preamble ["reagent/react.min.js"] + :optimizations :advanced + :pretty-print false}}}}}} + + :aliases {"uberjar" ["do" "clean" ["cljsbuild clean"] "uberjar"] + "cljsdev" ["do" ["cljsbuild" "clean"] ["cljsbuild" "once"] ["cljsbuild" "auto"]]}) diff --git a/examples/todomvc/resources/html/app.html b/examples/todomvc/resources/html/app.html new file mode 100644 index 0000000..0e31c8b --- /dev/null +++ b/examples/todomvc/resources/html/app.html @@ -0,0 +1,19 @@ + + + + + todomvc with reagent + + + + +

This will become todomvc when the ClojureScript is compiled

+ {% if dev %}{% endif %} + {% if dev %}{% endif %} + + {% if dev %}{% endif %} + + + \ No newline at end of file diff --git a/examples/todomvc/resources/public/todos.css b/examples/todomvc/resources/public/todos.css new file mode 100644 index 0000000..9095474 --- /dev/null +++ b/examples/todomvc/resources/public/todos.css @@ -0,0 +1,558 @@ +@charset "utf-8"; +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + color: inherit; + -webkit-appearance: none; + -ms-appearance: none; + -o-appearance: none; + appearance: none; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #eaeaea; + /* background: #eaeaea url('bg.png'); */ + color: #4d4d4d; + width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + -ms-font-smoothing: antialiased; + -o-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +button, +input[type="checkbox"] { + outline: none; +} + +#todoapp { + background: #fff; + background: rgba(255, 255, 255, 0.9); + margin: 130px 0 40px 0; + border: 1px solid #ccc; + position: relative; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.15); +} + +#todoapp:before { + content: ''; + border-left: 1px solid #f5d6d6; + border-right: 1px solid #f5d6d6; + width: 2px; + position: absolute; + top: 0; + left: 40px; + height: 100%; +} + +#todoapp input::-webkit-input-placeholder { + font-style: italic; +} + +#todoapp input::-moz-placeholder { + font-style: italic; + color: #a9a9a9; +} + +#todoapp h1 { + position: absolute; + top: -120px; + width: 100%; + font-size: 70px; + font-weight: bold; + text-align: center; + color: #b3b3b3; + color: rgba(255, 255, 255, 0.3); + text-shadow: -1px -1px rgba(0, 0, 0, 0.2); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + -ms-text-rendering: optimizeLegibility; + -o-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +#header { + padding-top: 15px; + border-radius: inherit; +} + +#header:before { + content: ''; + position: absolute; + top: 0; + right: 0; + left: 0; + height: 15px; + z-index: 2; + border-bottom: 1px solid #6c615c; + background: #8d7d77; + background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); + background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); + border-top-left-radius: 1px; + border-top-right-radius: 1px; +} + +#new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + -ms-font-smoothing: antialiased; + -o-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +#new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.02); + z-index: 2; + box-shadow: none; +} + +#main { + position: relative; + z-index: 2; + border-top: 1px dotted #adadad; +} + +label[for='toggle-all'] { + display: none; +} + +#toggle-all { + position: absolute; + top: -42px; + left: -4px; + width: 40px; + text-align: center; + /* Mobile Safari */ + border: none; +} + +#toggle-all:before { + content: '»'; + font-size: 28px; + color: #d9d9d9; + padding: 0 25px 7px; +} + +#toggle-all:checked:before { + color: #737373; +} + +#todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +#todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px dotted #ccc; +} + +#todo-list li:last-child { + border-bottom: none; +} + +#todo-list li.editing { + border-bottom: none; + padding: 0; +} + +#todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +#todo-list li.editing .view { + display: none; +} + +#todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + /* Mobile Safari */ + border: none; + -webkit-appearance: none; + -ms-appearance: none; + -o-appearance: none; + appearance: none; +} + +#todo-list li .toggle:after { + content: '✔'; + /* 40 + a couple of pixels visual adjustment */ + line-height: 43px; + font-size: 20px; + color: #d9d9d9; + text-shadow: 0 -1px 0 #bfbfbf; +} + +#todo-list li .toggle:checked:after { + color: #85ada7; + text-shadow: 0 1px 0 #669991; + bottom: 1px; + position: relative; +} + +#todo-list li label { + white-space: pre; + word-break: break-word; + padding: 15px 60px 15px 15px; + margin-left: 45px; + display: block; + line-height: 1.2; + -webkit-transition: color 0.4s; + transition: color 0.4s; +} + +#todo-list li.completed label { + color: #a9a9a9; + text-decoration: line-through; +} + +#todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 22px; + color: #a88a8a; + -webkit-transition: all 0.2s; + transition: all 0.2s; +} + +#todo-list li .destroy:hover { + text-shadow: 0 0 1px #000, + 0 0 10px rgba(199, 107, 107, 0.8); + -webkit-transform: scale(1.3); + -ms-transform: scale(1.3); + transform: scale(1.3); +} + +#todo-list li .destroy:after { + content: '✖'; +} + +#todo-list li:hover .destroy { + display: block; +} + +#todo-list li .edit { + display: none; +} + +#todo-list li.editing:last-child { + margin-bottom: -1px; +} + +#footer { + color: #777; + padding: 0 15px; + position: absolute; + right: 0; + bottom: -31px; + left: 0; + height: 20px; + z-index: 1; + text-align: center; +} + +#footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 31px; + left: 0; + height: 50px; + z-index: -1; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), + 0 6px 0 -3px rgba(255, 255, 255, 0.8), + 0 7px 1px -3px rgba(0, 0, 0, 0.3), + 0 43px 0 -6px rgba(255, 255, 255, 0.8), + 0 44px 2px -6px rgba(0, 0, 0, 0.2); +} + +#todo-count { + float: left; + text-align: left; +} + +#filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +#filters li { + display: inline; +} + +#filters li a { + color: #83756f; + margin: 2px; + text-decoration: none; +} + +#filters li a.selected { + font-weight: bold; +} + +#clear-completed { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + background: rgba(0, 0, 0, 0.1); + font-size: 11px; + padding: 0 10px; + border-radius: 3px; + box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); +} + +#clear-completed:hover { + background: rgba(0, 0, 0, 0.15); + box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); +} + +#info { + margin: 65px auto 0; + color: #a6a6a6; + font-size: 12px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); + text-align: center; +} + +#info a { + color: inherit; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox and Opera +*/ + +@media screen and (-webkit-min-device-pixel-ratio:0) { + #toggle-all, + #todo-list li .toggle { + background: none; + } + + #todo-list li .toggle { + height: 40px; + } + + #toggle-all { + top: -56px; + left: -15px; + width: 65px; + height: 41px; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +.hidden { + display: none; +} + +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #C5C5C5; + border-bottom: 1px dashed #F7F7F7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: '“'; + font-size: 50px; + opacity: .15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: '”'; + font-size: 50px; + opacity: .15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, .04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ''; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, .04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, .6); + -webkit-transition-property: left; + transition-property: left; + -webkit-transition-duration: 500ms; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + margin: 0 0 0 300px; + } + + .learn-bar > .learn { + left: 8px; + } + + .learn-bar #todoapp { + width: 550px; + margin: 130px auto 40px auto; + } +} \ No newline at end of file diff --git a/examples/todomvc/resources/public/todosanim.css b/examples/todomvc/resources/public/todosanim.css new file mode 100644 index 0000000..999d534 --- /dev/null +++ b/examples/todomvc/resources/public/todosanim.css @@ -0,0 +1,18 @@ + +.todoitem-enter { + opacity: 0.1; + transition: opacity .2s ease-in; +} + +.todoitem-enter.todoitem-enter-active { + opacity: 1; +} + +.todoitem-leave { + opacity: 0.8; + transition: opacity 0.2s ease-out; +} + +.todoitem-leave.todoitem-leave-active { + opacity: 0.1; +} \ No newline at end of file diff --git a/examples/todomvc/src/todomvc/client.cljs b/examples/todomvc/src/todomvc/client.cljs new file mode 100644 index 0000000..367c116 --- /dev/null +++ b/examples/todomvc/src/todomvc/client.cljs @@ -0,0 +1,105 @@ +(ns todomvc.client + (:require + [reagent.core :as reagent :refer [atom]] + [clj-browserchannel-messaging.client :as browserchannel] + [reagent-data-views.client.core :as rviews] + [reagent-data-views.client.component :refer [view-cursor] :refer-macros [defvc]] + [ajax.core :refer [POST]])) + +(defn add-todo [text] (POST "/todos/add" {:format :url :params {:title text}})) +(defn toggle [id] (POST "/todos/toggle" {:format :url :params {:id id}})) +(defn save [id title] (POST "/todos/update" {:format :url :params {:id id :title title}})) +(defn delete [id] (POST "/todos/delete" {:format :url :params {:id id}})) + +(defn complete-all [v] (POST "/todos/mark-all" {:format :url :params {:done? v}})) +(defn clear-done [] (POST "/todos/delete-all-done")) + +(defn todo-input [{:keys [title on-save on-stop]}] + (let [val (atom title) + stop #(do (reset! val "") + (if on-stop (on-stop))) + save #(let [v (-> @val str clojure.string/trim)] + (if-not (empty? v) (on-save v)) + (stop))] + (fn [props] + [:input (merge props + {:type "text" :value @val :on-blur save + :on-change #(reset! val (-> % .-target .-value)) + :on-key-down #(case (.-which %) + 13 (save) + 27 (stop) + nil)})]))) + +(def todo-edit (with-meta todo-input + {:component-did-mount #(.focus (reagent/dom-node %))})) + +(defn todo-stats [{:keys [filt active done]}] + (let [props-for (fn [name] + {:class (if (= name @filt) "selected") + :on-click #(reset! filt name)})] + [:div + [:span#todo-count + [:strong active] " " (case active 1 "item" "items") " left"] + [:ul#filters + [:li [:a (props-for :all) "All"]] + [:li [:a (props-for :active) "Active"]] + [:li [:a (props-for :done) "Completed"]]] + (when (pos? done) + [:button#clear-completed {:on-click clear-done} + "Clear completed " done])])) + +(defn todo-item [] + (let [editing (atom false)] + (fn [{:keys [id done title]}] + [:li {:class (str (if done "completed ") + (if @editing "editing"))} + [:div.view + [:input.toggle {:type "checkbox" :checked done + :on-change #(toggle id)}] + [:label {:on-double-click #(reset! editing true)} title] + [:button.destroy {:on-click #(delete id)}]] + (when @editing + [todo-edit {:class "edit" :title title + :on-save #(save id %) + :on-stop #(reset! editing false)}])]))) + +(defvc todo-app [props] + [[:todos]] + (let [filt (atom :all)] + (fn [] + (let [items (view-cursor :todos) + done (->> @items (filter :done) count) + active (- (count @items) done)] + [:div + [:section#todoapp + [:header#header + [:h1 "todos"] + [todo-input {:id "new-todo" + :placeholder "What needs to be done?" + :on-save add-todo}]] + (when (-> @items count pos?) + [:div + [:section#main + [:input#toggle-all {:type "checkbox" :checked (zero? active) + :on-change #(complete-all (pos? active))}] + [:label {:for "toggle-all"} "Mark all as complete"] + [:ul#todo-list + (for [todo (->> @items + (filter + (case @filt + :active (complement :done) + :done :done + :all identity)) + (sort-by :id))] + ^{:key (:id todo)} [todo-item todo])]] + [:footer#footer + [todo-stats {:active active :done done :filt filt}]]])] + [:footer#info + [:p "Double-click to edit a todo"]]])))) + +(defn ^:export run [] + (browserchannel/init! + :callback + (fn [] + (rviews/init!) + (reagent/render-component [todo-app] (.-body js/document))))) diff --git a/examples/todomvc/src/todomvc/server.clj b/examples/todomvc/src/todomvc/server.clj new file mode 100644 index 0000000..c2edf96 --- /dev/null +++ b/examples/todomvc/src/todomvc/server.clj @@ -0,0 +1,121 @@ +(ns todomvc.server + (:gen-class) + (:require + [compojure.core :refer [routes GET POST]] + [compojure.route :as route] + [net.thegeez.jetty-async-adapter :refer [run-jetty-async]] + [ring.middleware.defaults :refer [wrap-defaults site-defaults]] + [ring.util.response :refer [response]] + [clj-pebble.core :as pebble] + [clj-browserchannel-messaging.server :as browserchannel] + [environ.core :refer [env]] + [honeysql.helpers :refer :all] + [clojure.java.jdbc :as sql] + [reagent-data-views.server.core :as rviews :refer [views-config]] + [views.db.core :refer [vexec! with-view-transaction]])) + +(def db {:classname "org.postgresql.Driver" + :subprotocol "postgresql" + :subname "//localhost/todomvc" + :user "todomvc" + :password "s3cr3t"}) + + +;; View templates (functions which return HoneySQL SELECT query maps) + +(defn ^:refresh-only todos-view [] + (-> (select :id :title :done) + (from :todos))) + +; the keys in this map are the names of the views, which we can refer to in our cljs code +; (the key + args to the view fn combined make up a "view signature" or "view-sig" +; e.g. [:items] in this case as it has zero arguments. +(def views + {:todos {:fn #'todos-view}}) + + +;; SQL operations (affecting the views defined above, so we use vexec! instead of jdbc calls) + +(defn add-todo! [title] + (vexec! + @views-config + (-> (insert-into :todos) + (values [{:title title}]))) + (response "added todo")) + +(defn delete-todo! [id] + (vexec! + @views-config + (-> (delete-from :todos) + (where [:= :id id]))) + (response "deleted todo")) + +(defn update-todo! [id title] + (vexec! + @views-config + (-> (update :todos) + (sset {:title title}) + (where [:= :id id]))) + (response "updated todo")) + +(defn toggle-todo! [id] + ; note that we could have written this operation using a single UPDATE query, + ; but writing it this way also serves to demonstrate: + ; - using transactions with vexec! + ; - that the db connection is available under :db in the views config map and can + ; be used to run any ordinary query directly with jdbc (of course) + (with-view-transaction [vt @views-config] + (let [done? (:done (first (sql/query (:db vt) ["SELECT done FROM todos WHERE id = ?" id])))] + (vexec! + vt + (-> (update :todos) + (sset {:done (not done?)}) + (where [:= :id id])))) + (response "toggled todo"))) + +(defn mark-all! [done?] + (vexec! + @views-config + (-> (update :todos) + (sset {:done done?}))) + (response "completed all todos")) + +(defn delete-all-done! [] + (vexec! + @views-config + (-> (delete-from :todos) + (where [:= :done true]))) + (response "deleted all done todos")) + + +;; Compojure routes / Jetty server & main + +(def app-routes + (routes + ; db action routes + (POST "/todos/add" [title] (add-todo! title)) + (POST "/todos/delete" [id] (delete-todo! (Integer/parseInt id))) + (POST "/todos/update" [id title] (update-todo! (Integer/parseInt id) title)) + (POST "/todos/toggle" [id] (toggle-todo! (Integer/parseInt id))) + (POST "/todos/mark-all" [done?] (mark-all! (Boolean/parseBoolean done?))) + (POST "/todos/delete-all-done" [] (delete-all-done!)) + + (GET "/" [] (pebble/render-resource "html/app.html" {:dev (env :dev)})) + (route/resources "/") + (route/not-found "not found"))) + +(defn run-server [] + (browserchannel/init! + :middleware [rviews/views-middleware]) + (rviews/init! db views) + + (-> app-routes + (browserchannel/wrap-browserchannel) + (wrap-defaults (assoc-in site-defaults [:security :anti-forgery] false)) + (run-jetty-async + {:port 8080 + :auto-reload? true + :join? false}))) + +(defn -main [& args] + (run-server))