(ns cake.tasks.dependencies
  (:use cake cake.core cake.ant
        [cake.project :only [group]]
        [clojure.java.shell :only [sh]])
  (:import [org.apache.maven.artifact.ant DependenciesTask RemoteRepository WritePomTask Pom]
           [org.apache.tools.ant.taskdefs Copy Delete Move]
           [org.apache.maven.model Dependency Exclusion License]
           [java.io File]))

(def *exclusions* nil)

(def repositories
  [["clojure"           "http://build.clojure.org/releases"]
   ["clojure-snapshots" "http://build.clojure.org/snapshots"]
   ["clojars"           "http://clojars.org/repo"]
   ["maven"             "http://repo1.maven.org/maven2"]])

(defn os-name []
  (let [name (System/getProperty "os.name")]
    (condp #(.startsWith %2 %1) name
      "Linux"    "linux"
      "Mac OS X" "macosx"
      "SunOS"    "solaris"
      "Windows"  "windows"
      "unknown")))

(defn os-arch []
  (or (first (:arch *opts*))
      (*config* "project.arch")
      (let [arch (System/getProperty "os.arch")]
        (case arch
          "amd64" "x86_64"
          "i386"  "x86"
          arch))))

(defn add-license [task attrs]
  (when attrs
    (.addConfiguredLicense task
      (make License attrs))))

(defn add-repositories [task repositories]
  (doseq [[id url] repositories]
    (.addConfiguredRemoteRepository task
      (make RemoteRepository {:id id :url url}))))

(defn exclusion [dep]
  (make Exclusion {:group-id (group dep) :artifact-id (name dep)}))

(defn- add-dep [task dep]
  (if (instance? Pom task)
    (.addConfiguredDependency task dep)
    (.addDependency task dep)))

(defn add-dependencies [task deps]
  (doseq [[dep opts] deps]
    (add-dep task
      (make Dependency
        {:group-id    (group dep)
         :artifact-id (name dep)
         :version     (:version opts)
         :exclusions  (map exclusion (concat *exclusions* (:exclusions opts)))}))))

(defn subproject-path [dep]
  (when *config*
    (*config* (str "subproject." (name dep)))))

(defn add-jarset [task path exclusions]
  (let [exclusions (map #(re-pattern (str % "-\\d.*")) exclusions)]
    (doseq [jar (fileset-seq {:dir path :includes "*.jar"}) :let [name (.getName jar)]]
      (when (not-any? #(re-matches % name) exclusions)
        (add-fileset task {:file jar})))))

(defn install-subprojects []
  (doseq [type [:dependencies :dev-dependencies], [dep opts] (*project* type)]
    (when-let [path (subproject-path dep)]
      (binding [*root* path]
        (cake-exec "install")))))

(defn extract-native [jars dest]
  (doseq [jar jars]
    (ant Copy {:todir dest :flatten true}
         (add-zipfileset {:src jar :includes (format "native/%s/%s/*" (os-name) (os-arch))}))))

(defn fetch [deps dest]
  (when (seq deps)
    (let [ref-id (str "cake.deps.fileset." (.getName dest))]
      (ant DependenciesTask {:fileset-id ref-id :path-id (:name *project*)}
           (add-repositories (into repositories (:repositories *project*)))
           (add-dependencies deps))
      (ant Copy {:todir dest :flatten true}
           (.addFileset (get-reference ref-id)))))
  (extract-native
   (fileset-seq {:dir dest :includes "*.jar"})
   (str dest "/native")))

(defn make-pom []
  (let [refid "cake.pom"
        file  (file "pom.xml")
        attrs (select-keys *project* [:artifact-id :group-id :version :name :description])]
    (ant Pom (assoc attrs :id refid)
      (add-license (:license *project*))
      (add-dependencies (:dependencies *project*)))
    (ant WritePomTask {:pom-ref-id refid :file file})))

(defn fetch-deps []
  (log "Fetching dependencies...")
  (fetch (:dependencies *project*) (file "build/lib"))
  (binding [*exclusions* ['clojure 'clojure-contrib]]
    (fetch (:dev-dependencies *project*) (file "build/lib/dev")))
  (when (.exists (file "build/lib"))
    (ant Delete {:dir "lib"})
    (ant Move {:file "build/lib" :tofile "lib" :verbose true}))
  (invoke clean {})
  (bake-restart))

(defn stale-deps? [deps-str deps-file]
  (or (not (.exists deps-file)) (not= deps-str (slurp deps-file))))

(deftask pom "Generate pom.xml from project.clj."
  (when (or (newer? (file "project.clj") (file "pom.xml")) (= ["force"] (:pom *opts*)))
    (log "creating pom.xml")
    (make-pom)))

(deftask deps #{pom}
  "Fetch dependencies and dev-dependencies. Use 'cake deps force' to refetch."
  (let [deps-str  (prn-str (into (sorted-map) (select-keys *project* [:dependencies :dev-dependencies])))
        deps-file (file "lib" "deps.clj")]
    (if (or (stale-deps? deps-str deps-file) (= ["force"] (:deps *opts*)))
      (do (install-subprojects)
          (fetch-deps)
          (spit deps-file deps-str))
      (when (= ["force"] (:compile *opts*))
        (invoke clean {})))))

(ns cake.tasks.dependencies
  (:use cake cake.core cake.ant
        [cake.project :only [group]]
        [clojure.java.shell :only [sh]])
  (:import [org.apache.maven.artifact.ant DependenciesTask RemoteRepository WritePomTask Pom]
           [org.apache.tools.ant.taskdefs Copy Delete Move]
           [org.apache.maven.model Dependency Exclusion License]
           [java.io File]))

(def *exclusions* nil)

(def repositories
  [["clojure"           "http://build.clojure.org/releases"]
   ["clojure-snapshots" "http://build.clojure.org/snapshots"]
   ["clojars"           "http://clojars.org/repo"]
   ["maven"             "http://repo1.maven.org/maven2"]])

(defn os-name []
  (let [name (System/getProperty "os.name")]
    (condp #(.startsWith %2 %1) name
      "Linux"    "linux"
      "Mac OS X" "macosx"
      "SunOS"    "solaris"
      "Windows"  "windows"
      "unknown")))

(defn os-arch []
  (or (first (:arch *opts*))
      (*config* "project.arch")
      (let [arch (System/getProperty "os.arch")]
        (case arch
          "amd64" "x86_64"
          "i386"  "x86"
          arch))))

(defn add-license [task attrs]
  (when attrs
    (.addConfiguredLicense task
      (make License attrs))))

(defn add-repositories [task repositories]
  (doseq [[id url] repositories]
    (.addConfiguredRemoteRepository task
      (make RemoteRepository {:id id :url url}))))

(defn exclusion [dep]
  (make Exclusion {:group-id (group dep) :artifact-id (name dep)}))

(defn- add-dep [task dep]
  (if (instance? Pom task)
    (.addConfiguredDependency task dep)
    (.addDependency task dep)))

(defn add-dependencies [task deps]
  (doseq [[dep opts] deps]
    (add-dep task
      (make Dependency
        {:group-id    (group dep)
         :artifact-id (name dep)
         :version     (:version opts)
         :classifier  (:classifier opts)
         :exclusions  (map exclusion (concat *exclusions* (:exclusions opts)))}))))

(defn subproject-path [dep]
  (when *config*
    (*config* (str "subproject." (name dep)))))

(defn add-jarset [task path exclusions]
  (let [exclusions (map #(re-pattern (str % "-\\d.*")) exclusions)]
    (doseq [jar (fileset-seq {:dir path :includes "*.jar"}) :let [name (.getName jar)]]
      (when (not-any? #(re-matches % name) exclusions)
        (add-fileset task {:file jar})))))

(defn install-subprojects []
  (doseq [type [:dependencies :dev-dependencies], [dep opts] (*project* type)]
    (when-let [path (subproject-path dep)]
      (binding [*root* path]
        (cake-exec "install")))))

(defn extract-native [jars dest]
  (doseq [jar jars]
    (ant Copy {:todir dest :flatten true}
         (add-zipfileset {:src jar :includes (format "native/%s/%s/*" (os-name) (os-arch))}))))

(defn fetch [deps dest]
  (when (seq deps)
    (let [ref-id (str "cake.deps.fileset." (.getName dest))]
      (ant DependenciesTask {:fileset-id ref-id :path-id (:name *project*)}
           (add-repositories (into repositories (:repositories *project*)))
           (add-dependencies deps))
      (ant Copy {:todir dest :flatten true}
           (.addFileset (get-reference ref-id)))))
  (extract-native
   (fileset-seq {:dir dest :includes "*.jar"})
   (str dest "/native")))

(defn make-pom []
  (let [refid "cake.pom"
        file  (file "pom.xml")
        attrs (select-keys *project* [:artifact-id :group-id :version :name :description])]
    (ant Pom (assoc attrs :id refid)
      (add-license (:license *project*))
      (add-dependencies (:dependencies *project*)))
    (ant WritePomTask {:pom-ref-id refid :file file})))

(defn fetch-deps []
  (log "Fetching dependencies...")
  (fetch (:dependencies *project*) (file "build/lib"))
  (binding [*exclusions* ['clojure 'clojure-contrib]]
    (fetch (:dev-dependencies *project*) (file "build/lib/dev")))
  (when (.exists (file "build/lib"))
    (ant Delete {:dir "lib"})
    (ant Move {:file "build/lib" :tofile "lib" :verbose true}))
  (invoke clean {})
  (bake-restart))

(defn stale-deps? [deps-str deps-file]
  (or (not (.exists deps-file)) (not= deps-str (slurp deps-file))))

(deftask pom "Generate pom.xml from project.clj."
  (when (or (newer? (file "project.clj") (file "pom.xml")) (= ["force"] (:pom *opts*)))
    (log "creating pom.xml")
    (make-pom)))

(deftask deps #{pom}
  "Fetch dependencies and dev-dependencies. Use 'cake deps force' to refetch."
  (let [deps-str  (prn-str (into (sorted-map) (select-keys *project* [:dependencies :dev-dependencies])))
        deps-file (file "lib" "deps.clj")]
    (if (or (stale-deps? deps-str deps-file) (= ["force"] (:deps *opts*)))
      (do (install-subprojects)
          (fetch-deps)
          (spit deps-file deps-str))
      (when (= ["force"] (:compile *opts*))
        (invoke clean {})))))

add support for maven classifiers

add support for maven classifiers


(ns salava.core.handler
  (:require [clojure.tools.logging :as log]
            [compojure.api.sweet :refer :all]
            [compojure.route :as route]
            [salava.core.session :refer [wrap-app-session]]
            [salava.core.util :refer [get-base-path get-data-dir]]
            [salava.core.routes :refer [legacy-routes]]
            [salava.core.helper :refer [dump plugin-str]]
            [slingshot.slingshot :refer :all]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]
            [ring.middleware.session :refer [wrap-session]]
            [ring.middleware.flash :refer [wrap-flash]]
            [ring.middleware.session.cookie :refer [cookie-store]]
            [ring.middleware.webjars :refer [wrap-webjars]]))

(defn get-route-def [ctx plugin]
  (try+
    (let [sym (symbol (str "salava." (clojure.string/replace (plugin-str plugin) #"/" ".") ".routes/route-def"))]
      (require (symbol (namespace sym)) :reload)
      ((resolve sym) ctx))
    (catch Object _
      (log/info (str "no routes in plugin " plugin)))))

(defn resolve-routes [ctx]
  (apply routes (map (fn [p] (get-route-def ctx p)) (conj (get-in ctx [:config :core :plugins]) :core))))

(defn ignore-trailing-slash
  "Modifies the request uri before calling the handler.
  Removes a single trailing slash from the end of the uri if present.

  Useful for handling optional trailing slashes until Compojure's route matching syntax supports regex.
  Adapted from http://stackoverflow.com/questions/8380468/compojure-regex-for-matching-a-trailing-slash"
  [handler]
  (fn  [request]
    (let [uri (:uri request)]
      (handler (assoc request :uri (if (and (not (= "/" uri))
                                            (.endsWith uri "/"))
                                     (subs uri 0 (dec (count uri)))
                                     uri))))))

(def remove-x-frame-routes
  ["/badge/info/(\\d+)/embed"])

(defn remove-x-frame-options [handler]
  (fn [request]
    (let [uri (:uri request)]
      (if-let [response (handler request)]
        (if (some #(re-find (re-pattern %) (str uri)) remove-x-frame-routes)
          (update-in response [:headers] dissoc "X-Frame-Options")
          response)))))

(defn wrap-middlewares [ctx routes]
  (let [config (get-in ctx [:config :core])]
    (-> routes
        (ignore-trailing-slash)
        (wrap-webjars)
        (wrap-defaults (-> site-defaults
                           (assoc-in [:security :anti-forgery] false)
                           (assoc-in [:session] false)
                           (assoc-in [:static :files] (get-data-dir ctx))
                           ))
        (remove-x-frame-options)
        (wrap-flash)
        (wrap-app-session config))))

(defn handler [ctx]
  (let [main-routes (resolve-routes ctx)]
    (wrap-middlewares
      ctx
      (api
        (swagger-routes {:ui "/swagger-ui"
                         :info  {:version "0.1.0"
                               :title "Salava REST API"
                               :description ""
                               :contact  {:name "Discendum Oy"
                                          :email "contact@openbadgepassport.com"
                                          :url "http://salava.org"}
                               :license  {:name "Apache 2.0"
                                          :url "http://www.apache.org/licenses/LICENSE-2.0"}}
                       :tags  [{:name "badge", :description "plugin"}
                               {:name "file", :description "plugin"}
                               {:name "gallery", :description "plugin"}
                               {:name "page", :description "plugin"}
                               {:name "translator", :description "plugin"}
                               {:name "user", :description "plugin"}]})

        (context (get-base-path ctx) [] main-routes)

        (legacy-routes ctx)

        (route/not-found "404 Not found")))))

(ns salava.core.handler
  (:require [clojure.tools.logging :as log]
            [compojure.api.sweet :refer :all]
            [compojure.route :as route]
            [salava.core.session :refer [wrap-app-session]]
            [salava.core.util :refer [get-base-path get-data-dir]]
            [salava.core.routes :refer [legacy-routes]]
            [salava.core.helper :refer [dump plugin-str]]
            [slingshot.slingshot :refer :all]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]
            [ring.middleware.session :refer [wrap-session]]
            [ring.middleware.flash :refer [wrap-flash]]
            [ring.middleware.session.cookie :refer [cookie-store]]
            [ring.middleware.webjars :refer [wrap-webjars]]))

(defn get-route-def [ctx plugin]
  (try+
    (let [sym (symbol (str "salava." (clojure.string/replace (plugin-str plugin) #"/" ".") ".routes/route-def"))]
      (require (symbol (namespace sym)) :reload)
      ((resolve sym) ctx))
    (catch Object _
      (log/info (str "no routes in plugin " plugin)))))

(defn resolve-routes [ctx]
  (apply routes (map (fn [p] (get-route-def ctx p)) (conj (get-in ctx [:config :core :plugins]) :core))))

(defn ignore-trailing-slash
  "Modifies the request uri before calling the handler.
  Removes a single trailing slash from the end of the uri if present.

  Useful for handling optional trailing slashes until Compojure's route matching syntax supports regex.
  Adapted from http://stackoverflow.com/questions/8380468/compojure-regex-for-matching-a-trailing-slash"
  [handler]
  (fn  [request]
    (let [uri (:uri request)]
      (handler (assoc request :uri (if (and (not (= "/" uri))
                                            (.endsWith uri "/"))
                                     (subs uri 0 (dec (count uri)))
                                     uri))))))

(defn remove-x-frame-options [handler]
  "Remove X-Frame-Options header if route ends with '/embed'"
  (fn [request]
    (let [uri (:uri request)]
      (if-let [response (handler request)]
        (if (re-find (re-pattern "/embed$") (str uri))
          (update-in response [:headers] dissoc "X-Frame-Options")
          response)))))

(defn wrap-middlewares [ctx routes]
  (let [config (get-in ctx [:config :core])]
    (-> routes
        (ignore-trailing-slash)
        (wrap-webjars)
        (wrap-defaults (-> site-defaults
                           (assoc-in [:security :anti-forgery] false)
                           (assoc-in [:session] false)
                           (assoc-in [:static :files] (get-data-dir ctx))
                           ))
        (remove-x-frame-options)
        (wrap-flash)
        (wrap-app-session config))))

(defn handler [ctx]
  (let [main-routes (resolve-routes ctx)]
    (wrap-middlewares
      ctx
      (api
        (swagger-routes {:ui "/swagger-ui"
                         :info  {:version "0.1.0"
                               :title "Salava REST API"
                               :description ""
                               :contact  {:name "Discendum Oy"
                                          :email "contact@openbadgepassport.com"
                                          :url "http://salava.org"}
                               :license  {:name "Apache 2.0"
                                          :url "http://www.apache.org/licenses/LICENSE-2.0"}}
                       :tags  [{:name "badge", :description "plugin"}
                               {:name "file", :description "plugin"}
                               {:name "gallery", :description "plugin"}
                               {:name "page", :description "plugin"}
                               {:name "translator", :description "plugin"}
                               {:name "user", :description "plugin"}]})

        (context (get-base-path ctx) [] main-routes)

        (legacy-routes ctx)

        (route/not-found "404 Not found")))))

Remove X-Frame-Options header from all embeddable views


(ns clojure-rest.core
  (:require-macros
    [reagent.ratom :refer [reaction]])
  (:require
    [reagent.core :as r]
    [clojure-rest.api :as api]
    [clojure-rest.fake-data :as fake]
    [clojure-rest.utils :as utils]
  ))

; https://github.com/reagent-project/reagent/blob/master/src/reagent/core.cljs
; https://github.com/reagent-project/reagent-cookbook/blob/master/old-recipes/nvd3/README.md
; https://github.com/Day8/re-frame/wiki/When-do-components-update%3F
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn status->str
  [status]
  (case status
    :backlog "Backlog"
    :under-dev "In Progress"
    :done "Done"
    :else "Unknown"))

(defn category->color
  [category]
  (case category
    :bug-fix "#BD8D31"
    :enhancement "#3A7E28"
    :else "#eee"))

(defn card-side-color
  "Render the ribbon on the left of the card, that indicates its category"
  [card]
  {:position "absolute" :zindex -1 :top 0 :bottom 0 :left 0 :width 5
   :backgroundColor (-> card :category category->color)
  })

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def app-state
  (r/atom {:cards {}}))

(defn add-cards!
  "Add several cards to the application state"
  [cards]
  ; BUG - Keeping the :show-details at app-state means that rendering twice
  ; the component would lead to the GUI being updated at two places
  ; => It cannot be in the component alone (if you want the expand all)
  ;    But it should be in a state specific to the board (assoc container)
  (let [default-state {:show-details false}
        to-gui-card #(vector (:card-id %) (merge % default-state))]
    (swap! app-state
     #(update-in % [:cards] merge (map to-gui-card cards))
  )))

(defn toggle-all-cards
  [cards]
  (let [all-toggled (every? #(-> % second :show-details) cards)
        toggle-card #(assoc % :show-details (not all-toggled))]
    (utils/map-values toggle-card cards)
  ))

(defn filter-by-title
  "Keep only cards that contains the searched string inside their title"
  [title cards]
  (if (empty? title)
    cards
    (filter #(utils/lower-str-contains (-> % second :title) title) cards)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn render-task
  "[Pure] display a task you can check or delete"
  [task on-remove on-check]
  [:li.checklist__task
   [:input {:type "checkbox"
            :default-checked (:done task)
            :on-click on-check}]
   (:name task)
   [:a.checklist__task--remove {:href "#" :on-click on-remove}]
  ])

(defn render-tasks
  "[Pure] Display a list of tasks you can check or remove"
  [tasks on-remove on-check]
  [:div.checklist
   [:ul
    (map-indexed
      (fn [idx t]
        ^{:key t} [render-task t #(on-remove idx) #(on-check idx)])
      tasks
    )]
  ])

(defn render-add-task
  "[Pure] Render the text field to add new tasks to a card"
  [on-add]
  [:input.checklist--add-task
   {:type "text"
    :placeholder "Type then hit Enter to add a task"
    :on-key-press
    (fn [e]
      (when (= "Enter" (.-key e))
        (on-add (.. e -target -value))
        (set! (.. e -target -value) "")
      ))
   }])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn handle-drop
  [cards e status]
  (let [card-id (utils/get-transfer-data e :card-id)]
    (assoc-in cards [(int card-id) :status] status)
  ))

(defn event-handlers
  [cards]
  {:on-add-card #(js/alert "toto - use route to display the form")
   :on-toggle-all #(swap! cards toggle-all-cards)
   :on-toggle-card #(swap! cards update-in [%1 :show-details] not)
   :on-remove-task #(swap! cards update-in [%1] api/remove-task-at %2)
   :on-check-task #(swap! cards update-in [%1 :tasks %2 :done] not)
   :on-add-task #(swap! cards update-in [%1] api/add-task %2)
   :on-card-drop #(swap! cards handle-drop %1 %2)
  })

(defn render-card
  ; TODO - Try to remove the mess of call-backs
  "[Pure] Render a card"
  [{:keys [on-toggle-card on-remove-task on-check-task on-add-task]
    :as event-handlers}]
  (fn [{:keys [card-id title description show-details tasks]
        :as card}]
    (let [title-style (if show-details :div.card__title--is-open :div.card__title)
          on-drag-start #(utils/set-transfer-data % :card-id card-id)]
      [:div.card
       {:draggable true :onDragStart on-drag-start}
       [:div {:style (card-side-color card)}]
       [title-style {:on-click #(on-toggle-card card-id)} title]
       [:div.card__details
        (when-not show-details {:style {:display "none"}})
        description
        [render-tasks tasks #(on-remove-task card-id %) #(on-check-task card-id %)]
        [render-add-task #(on-add-task card-id %)]
      ]]
  )))

(defn render-column
  "[Pure] Render a column holding a set of cards"
  [card-renderer {:keys [on-card-drop] :as event-handlers}]
  (fn [status cards]
    [:div.column
     {:onDragOver #(.preventDefault %)
      :onDrop #(on-card-drop % status)}
     [:h1 (status->str status)]
     (for [c cards] ^{:key (:card-id c)} [card-renderer c])
    ]))

(defn render-board
  "[Pure] Render the dash-board as a set of column (one by status)"
  [column-rendered]
  (fn [cards]
    (let [cards-by-status (group-by :status (map second cards))]
      [:div.board
       (for [status [:backlog :under-dev :done]]
         ^{:key status} [column-rendered status (cards-by-status status)])
      ])
    ))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn render-filter
  "[Side effect] Render the filter to only show cards containing a given text"
  [filter-ref]
  [:input.search-input
    {:type "text" :placeholder "search"
     :value @filter-ref
     :on-change #(reset! filter-ref (.. % -target -value))}
  ])

(defn render-toggle-all
  "[Pure] Render the button to expand all cards"
  [on-toggle-all]
  (fn []
    [:button.header-button
     {:type "button" :on-click on-toggle-all} "Expand all"]
  ))

(defn render-add-card
  "[Pure] Render the button to expand add a new card"
  [on-add-card]
  (fn []
    [:button.header-button
     {:on-click on-add-card :type "button"} "Add card"]
  ))

(defn render-app
  []
  (let [filter (r/atom "")
        cards (r/cursor app-state [:cards])
        filtered (reaction (filter-by-title @filter @cards))
        handlers (event-handlers cards)
        card-renderer (render-card handlers)
        column-rendered (render-column card-renderer handlers)
        board-renderer (render-board column-rendered)]
    (fn []
      [:div
       (render-filter filter)
       [render-add-card (handlers :on-add-card)]
       [render-toggle-all (handlers :on-toggle-all)]
       [board-renderer @filtered]
      ])
    ))

(def fetch-and-render-app
  "Render the app - adding a fetching of data when the DOM is mounted"
  (with-meta render-app
    {:component-did-mount #(fake/fetch-cards! add-cards!)}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(r/render [fetch-and-render-app]
  (js/document.getElementById "app"))

(ns clojure-rest.core
  (:require-macros
    [reagent.ratom :refer [reaction]])
  (:require
    [reagent.core :as r]
    [clojure-rest.api :as api]
    [clojure-rest.fake-data :as fake]
    [clojure-rest.utils :as utils]
  ))

; https://github.com/reagent-project/reagent/blob/master/src/reagent/core.cljs
; https://github.com/reagent-project/reagent-cookbook/blob/master/old-recipes/nvd3/README.md
; https://github.com/Day8/re-frame/wiki/When-do-components-update%3F
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn status->str
  [status]
  (case status
    :backlog "Backlog"
    :under-dev "In Progress"
    :done "Done"
    :else "Unknown"))

(defn category->color
  [category]
  (case category
    :bug-fix "#BD8D31"
    :enhancement "#3A7E28"
    :else "#eee"))

(defn card-side-color
  "Render the ribbon on the left of the card, that indicates its category"
  [card]
  {:position "absolute" :zindex -1 :top 0 :bottom 0 :left 0 :width 5
   :backgroundColor (-> card :category category->color)
  })

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def app-state
  (r/atom {:cards {}}))

(defn add-cards!
  "Add several cards to the application state"
  [cards]
  ; BUG - Keeping the :show-details at app-state means that rendering twice
  ; the component would lead to the GUI being updated at two places
  ; => It cannot be in the component alone (if you want the expand all)
  ;    But it should be in a state specific to the board (assoc container)
  (let [default-state {:show-details false}
        to-gui-card #(vector (:card-id %) (merge % default-state))]
    (swap! app-state
     #(update-in % [:cards] merge (map to-gui-card cards))
  )))

(defn toggle-all-cards
  [cards]
  (let [all-toggled (every? #(-> % second :show-details) cards)
        toggle-card #(assoc % :show-details (not all-toggled))]
    (utils/map-values toggle-card cards)
  ))

(defn filter-by-title
  "Keep only cards that contains the searched string inside their title"
  [title cards]
  (if (empty? title)
    cards
    (filter #(utils/lower-str-contains (-> % second :title) title) cards)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn render-task
  "[Pure] display a task you can check or delete"
  [task on-remove on-check]
  [:li.checklist__task
   [:input {:type "checkbox"
            :default-checked (:done task)
            :on-click on-check}]
   (:name task)
   [:a.checklist__task--remove {:href "#" :on-click on-remove}]
  ])

(defn render-tasks
  "[Pure] Display a list of tasks you can check or remove"
  [tasks on-remove on-check]
  [:div.checklist
   [:ul
    (map-indexed
      (fn [idx t]
        ^{:key t} [render-task t #(on-remove idx) #(on-check idx)])
      tasks
    )]
  ])

(defn render-add-task
  "[Pure] Render the text field to add new tasks to a card"
  [on-add]
  [:input.checklist--add-task
   {:type "text"
    :placeholder "Type then hit Enter to add a task"
    :on-key-press
    (fn [e]
      (when (= "Enter" (.-key e))
        (on-add (.. e -target -value))
        (set! (.. e -target -value) "")
      ))
   }])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn handle-drop
  [cards e status]
  (let [card-id (utils/get-transfer-data e :card-id)]
    (assoc-in cards [(int card-id) :status] status)
  ))

(defn event-handlers
  [cards]
  {:on-add-card #(js/alert "toto - use route to display the form")
   :on-toggle-all #(swap! cards toggle-all-cards)
   :on-toggle-card #(swap! cards update-in [%1 :show-details] not)
   :on-remove-task #(swap! cards update-in [%1] api/remove-task-at %2)
   :on-check-task #(swap! cards update-in [%1 :tasks %2 :done] not)
   :on-add-task #(swap! cards update-in [%1] api/add-task %2)
   :on-card-drop #(swap! cards handle-drop %1 %2)
  })

(defn render-card
  ; TODO - Try to remove the mess of call-backs
  "[Pure] Render a card"
  [{:keys [on-toggle-card on-remove-task on-check-task on-add-task]
    :as event-handlers}]
  (fn [{:keys [card-id title description show-details tasks]
        :as card}]
    (let [title-style (if show-details :div.card__title--is-open :div.card__title)
          on-drag-start #(utils/set-transfer-data % :card-id card-id)]
      [:div.card
       {:draggable true :onDragStart on-drag-start}
       [:div {:style (card-side-color card)}]
       [title-style {:on-click #(on-toggle-card card-id)} title]
       [:div.card__details
        (when-not show-details {:style {:display "none"}})
        description
        [render-tasks tasks #(on-remove-task card-id %) #(on-check-task card-id %)]
        [render-add-task #(on-add-task card-id %)]
      ]]
  )))

(defn render-column
  "[Pure] Render a column holding a set of cards"
  [card-renderer on-card-drop]
  (fn [status cards]
    [:div.column
     {:onDragOver #(.preventDefault %)
      :onDrop #(on-card-drop % status)}
     [:h1 (status->str status)]
     (for [c cards] ^{:key (:card-id c)} [card-renderer c])
    ]))

(defn render-board
  "[Pure] Render the dash-board as a set of column (one by status)"
  [column-rendered]
  (fn [cards]
    (let [cards-by-status (group-by :status (map second cards))]
      [:div.board
       (for [status [:backlog :under-dev :done]]
         ^{:key status} [column-rendered status (cards-by-status status)])
      ])
    ))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn render-filter
  "[Side effect] Render the filter to only show cards containing a given text"
  [filter-ref]
  [:input.search-input
    {:type "text" :placeholder "search"
     :value @filter-ref
     :on-change #(reset! filter-ref (.. % -target -value))}
  ])

(defn render-toggle-all
  "[Pure] Render the button to expand all cards"
  [on-toggle-all]
  (fn []
    [:button.header-button
     {:type "button" :on-click on-toggle-all} "Expand all"]
  ))

(defn render-add-card
  "[Pure] Render the button to expand add a new card"
  [on-add-card]
  (fn []
    [:button.header-button
     {:on-click on-add-card :type "button"} "Add card"]
  ))

(defn render-app
  []
  (let [filter (r/atom "")
        cards (r/cursor app-state [:cards])
        filtered (reaction (filter-by-title @filter @cards))
        handlers (event-handlers cards)
        card-renderer (render-card handlers)
        column-rendered (render-column card-renderer (handlers :on-card-drop))
        board-renderer (render-board column-rendered)]
    (fn []
      [:div
       (render-filter filter)
       [render-add-card (handlers :on-add-card)]
       [render-toggle-all (handlers :on-toggle-all)]
       [board-renderer @filtered]
      ])
    ))

(def fetch-and-render-app
  "Render the app - adding a fetching of data when the DOM is mounted"
  (with-meta render-app
    {:component-did-mount #(fake/fetch-cards! add-cards!)}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(r/render [fetch-and-render-app]
  (js/document.getElementById "app"))

Simplify render-column

Simplify render-column


(ns clojure-rest.core
  (:require-macros
    [reagent.ratom :refer [reaction]])
  (:require
    [reagent.core :as r]
    [clojure-rest.api :as api]
    [clojure-rest.fake-data :as fake]
    [clojure-rest.utils :as utils]
  ))

; https://github.com/reagent-project/reagent/blob/master/src/reagent/core.cljs
; https://github.com/reagent-project/reagent-cookbook/blob/master/old-recipes/nvd3/README.md
; https://github.com/Day8/re-frame/wiki/When-do-components-update%3F
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn status->str
  [status]
  (case status
    :backlog "Backlog"
    :under-dev "In Progress"
    :done "Done"
    :else "Unknown"))

(defn category->color
  [category]
  (case category
    :bug-fix "#BD8D31"
    :enhancement "#3A7E28"
    :else "#eee"))

(defn card-side-color
  "Render the ribbon on the left of the card, that indicates its category"
  [card]
  {:position "absolute" :zindex -1 :top 0 :bottom 0 :left 0 :width 5
   :backgroundColor (-> card :category category->color)
  })

(defn filter-by-status
  "Filter card by status"
  [status cards]
  (filter #(= status (:status %)) (map second cards)))

 (defn filter-by-title
   "Keep only cards that contains the searched string inside their title"
   [title cards]
   (if (empty? title)
     cards
     (filter #(utils/str-contains (-> % second :title) title) cards)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def app-state
  (r/atom {:cards {}}))

(defn add-cards!
  "Add several cards to the application state"
  [cards]
  ; BUG - Keeping the :show-details at app-state means that rendering twice
  ; the component would lead to the GUI being updated at two places
  ; => It cannot be in the component alone (if you want the expand all)
  ;    But it should be in a state specific to the board (assoc container)
  (let [default-state {:show-details false}
        to-gui-card #(vector (:card-id %) (merge % default-state))]
    (swap! app-state
     #(update-in % [:cards] merge (map to-gui-card cards))
  )))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn render-task
  "[Pure] display a task you can check or delete"
  [task on-remove on-check]
  [:li.checklist__task
   [:input {:type "checkbox"
            :default-checked (:done task)
            :on-click on-check}]
   (:name task)
   [:a.checklist__task--remove {:href "#" :on-click on-remove}]
  ])

(defn render-tasks
  "[Pure] Display a list of tasks you can check or remove"
  [tasks on-remove on-check]
  [:div.checklist
   [:ul
    (map-indexed
      (fn [idx t]
        ^{:key t} [render-task t #(on-remove idx) #(on-check idx)])
      tasks
    )]
  ])

(defn render-add-task
  "[Pure] Render the text field to add new tasks to a card"
  [on-add]
  [:input.checklist--add-task
   {:type "text"
    :placeholder "Type then hit Enter to add a task"
    :on-key-press
    (fn [e]
      (when (= "Enter" (.-key e))
        (on-add (.. e -target -value))
        (set! (.. e -target -value) "")
      ))
   }])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn render-card
  ; TODO - Try to remove the mess of call-backs
  "[Pure] Render a card"
  [on-toggle-card on-remove-task on-check-task on-add-task]
  (fn [card]
    (let [card-id (:card-id card)
          details-style (when-not (:show-details card) {:style {:display "none"}})
          title-style (if (:show-details card) :div.card__title--is-open :div.card__title)]
      [:div.card
       [:div {:style (card-side-color card)}]
       [title-style {:on-click #(on-toggle-card card-id)} (:title card)]
       [:div.card__details details-style
        (:description card)
        [render-tasks (:tasks card) #(on-remove-task card-id %) #(on-check-task card-id %)]
        [render-add-task #(on-add-task card-id %)]
      ]]
  )))

(defn render-column
  [status cards card-renderer]
  [:div.list
   [:h1 (status->str status)]
   (for [c (filter-by-status status cards)]
     ^{:key (:card-id c)} [card-renderer c])
  ])

(defn render-board
  ; TODO - Use group by instead of filter
  [cards card-renderer]
  [:div.app
   (for [status [:backlog :under-dev :done]]
     ^{:key status} [render-column status cards card-renderer])
  ])

(defn render-filter
  [filter-ref]
  [:input.search-input
    {:type "text" :placeholder "search"
     :value @filter-ref
     :on-change #(reset! filter-ref (.. % -target -value))}
  ])

(defn render-toggle-all
  [on-toggle-all]
  (fn []
    [:button.header-button
     {:type "button" :on-click on-toggle-all} "Expand all"]
  ))

(defn render-add-card
  [on-add-card]
  (fn []
    [:button.header-button
     {:on-click on-add-card :type "button"} "Add card"]
  ))

(defn toggle-all-cards
  ; TODO - Rework... the map is not nice
  [cards]
  (let [all-toggled (every? #(-> % second :show-details) cards)
        toggle-card #(assoc % :show-details (not all-toggled))]
    (utils/map-values toggle-card cards)
  ))

(defn render-app
  []
  ; TODO - Do not make such a big tree of functions
  ; - The DOM needs to be that deep, but not functions
  ; - But you can create the card at the top
  ; - And then you can assemble them (group-by or filter)
  (let [filter (r/atom "")]
    (fn []
      (let [cards (r/cursor app-state [:cards])
            filtered (reaction (filter-by-title @filter @cards))

            on-add-card #(js/alert "toto - use route to display the form")
            on-toggle-all #(swap! cards toggle-all-cards)
            on-toggle-card #(swap! cards update-in [%1 :show-details] not)
						on-remove-task #(swap! cards update-in [%1] api/remove-task-at %2)
						on-check-task #(swap! cards update-in [%1 :tasks %2 :done] not)
						on-add-task #(swap! cards update-in [%1] api/add-task %2)
            ]
        [:div
         (render-filter filter)
         [render-add-card on-add-card]
         [render-toggle-all on-toggle-all]
         [render-board @filtered
          (render-card on-toggle-card on-remove-task on-check-task on-add-task)]
        ]))
  ))

(def fetch-and-render-app
  "Render the app - adding a fetching of data when the DOM is mounted"
  (with-meta render-app
    {:component-did-mount #(fake/fake-fetch! add-cards!)}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(r/render [fetch-and-render-app]
  (js/document.getElementById "app"))

(ns clojure-rest.core
  (:require-macros
    [reagent.ratom :refer [reaction]])
  (:require
    [reagent.core :as r]
    [clojure-rest.api :as api]
    [clojure-rest.fake-data :as fake]
    [clojure-rest.utils :as utils]
  ))

; https://github.com/reagent-project/reagent/blob/master/src/reagent/core.cljs
; https://github.com/reagent-project/reagent-cookbook/blob/master/old-recipes/nvd3/README.md
; https://github.com/Day8/re-frame/wiki/When-do-components-update%3F
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn status->str
  [status]
  (case status
    :backlog "Backlog"
    :under-dev "In Progress"
    :done "Done"
    :else "Unknown"))

(defn category->color
  [category]
  (case category
    :bug-fix "#BD8D31"
    :enhancement "#3A7E28"
    :else "#eee"))

(defn card-side-color
  "Render the ribbon on the left of the card, that indicates its category"
  [card]
  {:position "absolute" :zindex -1 :top 0 :bottom 0 :left 0 :width 5
   :backgroundColor (-> card :category category->color)
  })

(defn filter-by-title
  "Keep only cards that contains the searched string inside their title"
  [title cards]
  (if (empty? title)
    cards
    (filter #(utils/str-contains (-> % second :title) title) cards)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def app-state
  (r/atom {:cards {}}))

(defn add-cards!
  "Add several cards to the application state"
  [cards]
  ; BUG - Keeping the :show-details at app-state means that rendering twice
  ; the component would lead to the GUI being updated at two places
  ; => It cannot be in the component alone (if you want the expand all)
  ;    But it should be in a state specific to the board (assoc container)
  (let [default-state {:show-details false}
        to-gui-card #(vector (:card-id %) (merge % default-state))]
    (swap! app-state
     #(update-in % [:cards] merge (map to-gui-card cards))
  )))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn render-task
  "[Pure] display a task you can check or delete"
  [task on-remove on-check]
  [:li.checklist__task
   [:input {:type "checkbox"
            :default-checked (:done task)
            :on-click on-check}]
   (:name task)
   [:a.checklist__task--remove {:href "#" :on-click on-remove}]
  ])

(defn render-tasks
  "[Pure] Display a list of tasks you can check or remove"
  [tasks on-remove on-check]
  [:div.checklist
   [:ul
    (map-indexed
      (fn [idx t]
        ^{:key t} [render-task t #(on-remove idx) #(on-check idx)])
      tasks
    )]
  ])

(defn render-add-task
  "[Pure] Render the text field to add new tasks to a card"
  [on-add]
  [:input.checklist--add-task
   {:type "text"
    :placeholder "Type then hit Enter to add a task"
    :on-key-press
    (fn [e]
      (when (= "Enter" (.-key e))
        (on-add (.. e -target -value))
        (set! (.. e -target -value) "")
      ))
   }])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn render-card
  ; TODO - Try to remove the mess of call-backs
  "[Pure] Render a card"
  [on-toggle-card on-remove-task on-check-task on-add-task]
  (fn [card]
    (let [card-id (:card-id card)
          details-style (when-not (:show-details card) {:style {:display "none"}})
          title-style (if (:show-details card) :div.card__title--is-open :div.card__title)]
      [:div.card
       [:div {:style (card-side-color card)}]
       [title-style {:on-click #(on-toggle-card card-id)} (:title card)]
       [:div.card__details details-style
        (:description card)
        [render-tasks (:tasks card) #(on-remove-task card-id %) #(on-check-task card-id %)]
        [render-add-task #(on-add-task card-id %)]
      ]]
  )))

(defn render-column
  [card-renderer status cards]
  [:div.list
   [:h1 status]
   (for [c cards]
     ^{:key (:card-id c)} [card-renderer c])
  ])

(defn render-board
  [card-renderer cards]
  (let [cards-by-status (group-by :status (map second cards))]
    [:div.app
     (for [status [:backlog :under-dev :done]]
       ^{:key status} [render-column card-renderer (status->str status) (cards-by-status status)])
    ]))

(defn render-filter
  [filter-ref]
  [:input.search-input
    {:type "text" :placeholder "search"
     :value @filter-ref
     :on-change #(reset! filter-ref (.. % -target -value))}
  ])

(defn render-toggle-all
  [on-toggle-all]
  (fn []
    [:button.header-button
     {:type "button" :on-click on-toggle-all} "Expand all"]
  ))

(defn render-add-card
  [on-add-card]
  (fn []
    [:button.header-button
     {:on-click on-add-card :type "button"} "Add card"]
  ))

(defn toggle-all-cards
  ; TODO - Rework... the map is not nice
  [cards]
  (let [all-toggled (every? #(-> % second :show-details) cards)
        toggle-card #(assoc % :show-details (not all-toggled))]
    (utils/map-values toggle-card cards)
  ))

(defn render-app
  []
  ; TODO - Do not make such a big tree of functions
  ; - The DOM needs to be that deep, but not functions
  ; - But you can create the card at the top
  ; - And then you can assemble them (group-by or filter)
  (let [filter (r/atom "")]
    (fn []
      (let [cards (r/cursor app-state [:cards])
            filtered (reaction (filter-by-title @filter @cards))

            on-add-card #(js/alert "toto - use route to display the form")
            on-toggle-all #(swap! cards toggle-all-cards)
            on-toggle-card #(swap! cards update-in [%1 :show-details] not)
						on-remove-task #(swap! cards update-in [%1] api/remove-task-at %2)
						on-check-task #(swap! cards update-in [%1 :tasks %2 :done] not)
						on-add-task #(swap! cards update-in [%1] api/add-task %2)
            ]
        [:div
         (render-filter filter)
         [render-add-card on-add-card]
         [render-toggle-all on-toggle-all]
         [render-board
          (render-card on-toggle-card on-remove-task on-check-task on-add-task)
          @filtered]
        ]))
  ))

(def fetch-and-render-app
  "Render the app - adding a fetching of data when the DOM is mounted"
  (with-meta render-app
    {:component-did-mount #(fake/fake-fetch! add-cards!)}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(r/render [fetch-and-render-app]
  (js/document.getElementById "app"))

Use group by instead of filtering by status


(ns clojure-rest.core
  (:require-macros
    [reagent.ratom :refer [reaction]])
  (:require
    [reagent.core :as r]
    [clojure-rest.api :as api]
    [clojure-rest.fake-data :as fake]
    [clojure-rest.utils :as utils]
  ))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn status->str
  [status]
  (condp = status
    :backlog "Backlog"
    :under-dev "In Progress"
    :done "Done"
    :else "Unknown"))

(defn category->color
  [category]
  (condp = category
    :bug-fix "#BD8D31"
    :enhancement "#3A7E28"
    :else "#eee"))

(defn card-side-color
  "Render the ribbon on the left of the card, that indicates its category"
  [card]
  {:position "absolute" :zindex -1 :top 0 :bottom 0 :left 0 :width 5
   :backgroundColor (-> card :category category->color)
  })

(defn filter-by-status
  [status cards]
  (filter #(= status (:status %)) cards))

(defn filter-by-title
  "Keep only cards that contains the searched string inside their title"
  [title cards]
  (if (empty? title)
    cards
    (filter #(utils/str-contains (-> % second :title) title) cards)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def app-state
  (r/atom
    {:cards {}
     :filter ""
    }))

(def card-list
  "FRP style zoom on the app state to render"
  (reaction
    (filter-by-title (:filter @app-state) (:cards @app-state))
  ))

(defn update-filter!
  "Update the filter used in the app state"
  [filter]
  (swap! app-state #(assoc % :filter filter)))

(defn update-card!
  "Update a card"
  [card]
  (swap! app-state
    #(assoc-in % [:cards (:card-id card)] card)
  ))

(defn add-cards!
  "Add several cards to the application state"
  [cards]
  (let [to-id-pair #(vector (:card-id %) %)]
    (swap! app-state
     #(update-in % [:cards] merge (map to-id-pair cards))
  )))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn render-task
  [on-remove on-check task]
  [:li.checklist__task
   [:input {:type "checkbox"
            :default-checked (:done task)
            :on-click on-check}]
   (:name task)
   [:a.checklist__task--remove {:href "#" :on-click on-remove}]
  ])

(defn render-tasks
  [{:keys [tasks] :as card}]
  (let [on-remove #(update-card! (api/remove-task-at card %))
        on-check #(update-card! (update-in card [:tasks % :done] not))]
    [:div.checklist
     [:ul
      (map
        (fn [idx t]
          ^{:key t} [render-task #(on-remove idx) #(on-check idx) t])
        (range) tasks)]
     ]))

(defn render-add-task
  "Render the text field allowing to add new tasks to a card"
  [card]
  [:input.checklist--add-task
   {:type "text"
    :placeholder "Type then hit Enter to add a task"
    :on-key-press
    (fn [e]
      (when (= "Enter" (.-key e))
        (update-card! (api/add-task card (.. e -target -value)))
        (set! (.. e -target -value) "")
      ))
   }])

(defn render-card
  []
  (let [show-details (r/atom false) ; TODO - Extract show details in set of card-id
        toggle-details #(swap! show-details not)
        title-style #(if % :div.card__title--is-open :div.card__title)]
    (fn [card]
      [:div.card
       [:div {:style (card-side-color card)}]
       [(title-style @show-details) {:on-click toggle-details} (:title card)]
       (when @show-details
         [:div.card__details
          (:description card)
          [render-tasks card]
          [render-add-task card]
         ])
       ])
    ))

(defn render-list
  [status cards]
  [:div.list
   [:h1 (status->str status)]
   (for [c (filter-by-status status (map second cards))]
     ^{:key (:card-id c)} [render-card c])
  ])

(defn render-board
  [cards]
  [:div.app
   (for [status [:backlog :under-dev :done]]
     ^{:key status} [render-list status cards])
  ])

(defn render-app
  []
  [:div
   [:input.search-input
    {:type "text"
     :placeholder "search"
     :value (:filter @app-state)
     :on-change #(update-filter! (.. % -target -value))}]
   [render-board @card-list]
  ])

(def fetch-and-render-app
  "Render the app - adding a fetching of data when the DOM is mounted"
  (with-meta render-app
    {:component-did-mount #(fake/fake-fetch! add-cards!)}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(r/render [fetch-and-render-app]
  (js/document.getElementById "app"))

(ns clojure-rest.core
  (:require-macros
    [reagent.ratom :refer [reaction]])
  (:require
    [reagent.core :as r]
    [clojure-rest.api :as api]
    [clojure-rest.fake-data :as fake]
    [clojure-rest.utils :as utils]
  ))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn status->str
  [status]
  (condp = status
    :backlog "Backlog"
    :under-dev "In Progress"
    :done "Done"
    :else "Unknown"))

(defn category->color
  [category]
  (condp = category
    :bug-fix "#BD8D31"
    :enhancement "#3A7E28"
    :else "#eee"))

(defn card-side-color
  "Render the ribbon on the left of the card, that indicates its category"
  [card]
  {:position "absolute" :zindex -1 :top 0 :bottom 0 :left 0 :width 5
   :backgroundColor (-> card :category category->color)
  })

(defn filter-by-status
  [status cards]
  (filter #(= status (:status %)) cards))

(defn filter-by-title
  "Keep only cards that contains the searched string inside their title"
  [title cards]
  (if (empty? title)
    cards
    (filter #(utils/str-contains (-> % second :title) title) cards)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def app-state
  (r/atom
    {:cards {}
     :filter ""
    }))

(def card-list
  "FRP style zoom on the app state to render"
  (reaction
    (filter-by-title (:filter @app-state) (:cards @app-state))
  ))

(defn update-card!
  "Update a card"
  [card]
  (swap! app-state
    #(assoc-in % [:cards (:card-id card)] card)
  ))

(defn add-cards!
  "Add several cards to the application state"
  [cards]
  (let [to-id-pair #(vector (:card-id %) %)]
    (swap! app-state
     #(update-in % [:cards] merge (map to-id-pair cards))
  )))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn render-task
  [on-remove on-check task]
  [:li.checklist__task
   [:input {:type "checkbox"
            :default-checked (:done task)
            :on-click on-check}]
   (:name task)
   [:a.checklist__task--remove {:href "#" :on-click on-remove}]
  ])

(defn render-tasks
  [{:keys [tasks] :as card}]
  (let [on-remove #(update-card! (api/remove-task-at card %))
        on-check #(update-card! (update-in card [:tasks % :done] not))]
    [:div.checklist
     [:ul
      (map
        (fn [idx t]
          ^{:key t} [render-task #(on-remove idx) #(on-check idx) t])
        (range) tasks)]
     ]))

(defn render-add-task
  "Render the text field allowing to add new tasks to a card"
  [card]
  [:input.checklist--add-task
   {:type "text"
    :placeholder "Type then hit Enter to add a task"
    :on-key-press
    (fn [e]
      (when (= "Enter" (.-key e))
        (update-card! (api/add-task card (.. e -target -value)))
        (set! (.. e -target -value) "")
      ))
   }])

(defn render-card
  []
  (let [show-details (r/atom false) ; TODO - Extract show details in set of card-id
        toggle-details #(swap! show-details not)
        title-style #(if % :div.card__title--is-open :div.card__title)]
    (fn [card]
      [:div.card
       [:div {:style (card-side-color card)}]
       [(title-style @show-details) {:on-click toggle-details} (:title card)]
       (when @show-details
         [:div.card__details
          (:description card)
          [render-tasks card]
          [render-add-task card]
         ])
       ])
    ))

(defn render-list
  [status cards]
  [:div.list
   [:h1 (status->str status)]
   (for [c (filter-by-status status (map second cards))]
     ^{:key (:card-id c)} [render-card c])
  ])

(defn render-board
  [cards]
  [:div.app
   (for [status [:backlog :under-dev :done]]
     ^{:key status} [render-list status cards])
  ])

(defn render-filter
  [filter-cursor]
  [:input.search-input
    {:type "text" :placeholder "search"
     :value @filter-cursor
     :on-change #(reset! filter-cursor (.. % -target -value))}
  ])

(defn render-app
  []
  (let [filter (r/cursor app-state [:filter])]
    [:div
     (render-filter filter)
     [render-board @card-list]]
  ))

(def fetch-and-render-app
  "Render the app - adding a fetching of data when the DOM is mounted"
  (with-meta render-app
    {:component-did-mount #(fake/fake-fetch! add-cards!)}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(r/render [fetch-and-render-app]
  (js/document.getElementById "app"))

Use a cursor for the filter in the app-state


(ns comic-reader.main
  (:require [comic-reader.api :as api]
            [comic-reader.handlers :refer [init-handlers!]]
            [comic-reader.subscriptions
             :refer [init-subscriptions!]]
            [comic-reader.history :as history]
            [reagent.core :as reagent :refer [atom]]
            [re-frame.core :as rf]
            [secretary.core :as secretary
                            :refer-macros [defroute]]))

;; Have secretary pull apart URL's and then dispatch with re-frame
(defroute sites-path "/" []
  (rf/dispatch [:sites]))

(defroute comics-path "/comics/:site" [site]
  (rf/dispatch [:comics site]))

(defroute read-path "/read/:comic/:volume/:page" {:as location}
  (rf/dispatch [:read location]))

(defroute "*" {:as _}
  (rf/dispatch [:unknown]))

;; Actual re-frame code

(defn four-oh-four []
  [:div
   [:h1 "Sorry!"]
   "There's nothing to see here."
   [:a {:href "/#"}]])

(defn manga-site [site]
  (let [{:keys [id name]} site]
   ^{:key id}
     [:li
      [:input {:type "button"
               :value name
               :on-click #(.log js/console
                                (str "You clicked the "
                                     id
                                     " button!"))}]]))

(defn site-list []
  (let [site-list (rf/subscribe [:site-list])]
    (fn []
      (when-let [site-list @site-list]
        [:ul (map manga-site site-list)]))))

(defn comic-btn [comic]
  ^{:key (:name comic)}
  [:li (:name comic)])

(defn comic-list []
  (let [comics-list (rf/subscribe [:comic-list])]
    (fn []
      (when-let [comic-list @comic-list]
        [:ul (map comic-btn comic-list)]))))

(defn comic-reader []
  (let [page (rf/subscribe [:page])]
    (fn []
      [:div
       (case @page
         :sites [site-list]
         :comics [comic-list]
         :read "Display the comic itself!"
         nil ""
         [four-oh-four])])))

(defn ^:export run []
  (init-handlers!)
  (init-subscriptions!)
  (try
    (history/hook-browser-navigation!)
    (catch js/Error e
      nil))
  (reagent/render [comic-reader]
                  (.-body js/document)))

(ns comic-reader.main
  (:require [comic-reader.api :as api]
            [comic-reader.handlers :refer [init-handlers!]]
            [comic-reader.subscriptions
             :refer [init-subscriptions!]]
            [comic-reader.history :as history]
            [reagent.core :as reagent :refer [atom]]
            [re-frame.core :as rf]
            [secretary.core :as secretary
                            :refer-macros [defroute]]))

;; Have secretary pull apart URL's and then dispatch with re-frame
(defroute sites-path "/" []
  (rf/dispatch [:sites]))

(defroute comics-path "/comics/:site" [site]
  (rf/dispatch [:comics site]))

(defroute read-path "/read/:comic/:volume/:page" {:as location}
  (rf/dispatch [:read location]))

(defroute "*" {:as _}
  (rf/dispatch [:unknown]))

;; Actual re-frame code

(defn four-oh-four []
  [:div
   [:h1 "Sorry!"]
   "There's nothing to see here."
   [:a {:href "/#"}]])

(defn btn-for-id-name [site]
  (let [{:keys [id name]} site]
   ^{:key id}
     [:li
      [:input {:type "button"
               :value name
               :on-click #(.log js/console
                                (str "You clicked the "
                                     id
                                     " button!"))}]]))

(defn site-list []
  (let [site-list (rf/subscribe [:site-list])]
    (fn []
      (when-let [site-list @site-list]
        [:ul (map btn-for-id-name site-list)]))))

(defn comic-list []
  (let [comics-list (rf/subscribe [:comic-list])]
    (fn []
      (when-let [comic-list @comic-list]
        [:ul (map btn-for-id-name comic-list)]))))

(defn comic-reader []
  (let [page (rf/subscribe [:page])]
    (fn []
      [:div
       (case @page
         :sites [site-list]
         :comics [comic-list]
         :read "Display the comic itself!"
         nil ""
         [four-oh-four])])))

(defn ^:export run []
  (init-handlers!)
  (init-subscriptions!)
  (try
    (history/hook-browser-navigation!)
    (catch js/Error e
      nil))
  (reagent/render [comic-reader]
                  (.-body js/document)))

Use the same function for making many buttons


(ns potok-rumu.events
  (:require [potok.core :as ptk]
            [beicon.core :as rx]))

(deftype ^:private SmallShot []
  ptk/UpdateEvent
  (update [_ state]
    (update state :potok (fnil + 0) 0.2)))

(deftype ^:private BigShot []
  ptk/UpdateEvent
  (update [_ state]
    (update state :potok (fnil + 0) 0.5)))

(deftype ^:private Drain []
  ptk/UpdateEvent
  (update [_ state]
    (assoc state :potok 0.0)))

(deftype ^:private GoHome []
  ptk/WatchEvent
  (watch [_ state stream]
    (->> (rx/just (->Drain))
         (rx/delay 2000))))

(def ^:private events-map
  {:small-shot ->SmallShot
   :big-shot ->BigShot
   :go-home ->GoHome})

(defn emit-to!
  [^BehaviorSubject store ^Keyword event]
  (ptk/emit! store ((event events-map))))

(ns potok-rumu.events
  (:require [potok.core :as ptk]
            [beicon.core :as rx]))

(deftype ^:private Drink [^Number volume]
  ptk/UpdateEvent
  (update [_ state]
    (update state :potok (fnil + 0) volume)))

(deftype ^:private SmallShot []
  ptk/WatchEvent
  (watch [_ _ _]
    (rx/just (->Drink 0.2))))

(deftype ^:private BigShot []
  ptk/WatchEvent
  (watch [_ _ _]
    (rx/just (->Drink 0.5))))

(deftype ^:private Drain []
  ptk/UpdateEvent
  (update [_ state]
    (assoc state :potok 0.0)))

(deftype ^:private Sleep []
  ptk/EffectEvent
  (effect [_ _ _]
    (js/alert "Pššššš, už spí!")))

(deftype ^:private GoHome []
  ptk/WatchEvent
  (watch [_ state stream]
    (rx/merge
     (->> (rx/just (->Drain))
          (rx/delay 2000))
     (->> (rx/just(->Sleep))
          (rx/delay 2500)))))

(def ^:private events-map
  {:small-shot ->SmallShot
   :big-shot ->BigShot
   :go-home ->GoHome})

(defn emit-to!
  [^BehaviorSubject store ^Keyword event]
  (ptk/emit! store ((event events-map))))

Add example of effect event and refactor shots (#2)


(ns weather-demo.core
    (:require [reagent.core :as reagent :refer [atom]]
              [reagent.session :as session]
              [secretary.core :as secretary :include-macros true]
              [accountant.core :as accountant]

              [reagent-forms.core :refer [bind-fields]]
              [ajax.core :refer [GET json-response-format]]
              [clojure.string :as string]))

(def app-state (atom {}))
(def cities (reagent/cursor app-state [:cities]))
(def city-id (reagent/cursor app-state [:city :id]))

;; -------------------------
;; Components

(defn hc-chart-draw [this]
  (let [node (reagent/dom-node this)
        {:keys [config]} (reagent/props this)]
    (when config
      (js/Highcharts.Chart.
        (clj->js (assoc-in config [:chart :renderTo] node))))))

(defn hc-chart [config]
  (reagent/create-class {:display-name "highcharts"
                         :reagent-render (fn [] [:div])
                         :component-did-mount hc-chart-draw
                         :component-did-update hc-chart-draw}))

;; -------------------------
;; Views

(defn country-name [country]
  (let [name (-> country :name :common)
        region (-> country :region)]
    (string/join ", " (remove empty? [name region]))))

(defn country-selector [countries]
  [:select {:field :list :id :id}
   (for [country (sort-by country-name countries)]
     [:option {:key (:cca2 country)} (country-name country)])])

(defn country-field [countries country-atom]
  (println "Rendering country field for" (count countries) "countries")
  (if-not countries [:select [:option "Loading countries..."]]
    [bind-fields (country-selector countries) country-atom]))

(defn country-description [countries country-id]
  (println "Rendering country description for" country-id)
  (if-not country-id [:p "No country selected"]
    (let [country (first (filter #(= (:cca2 %) country-id) countries))]
      [:p "Selected country is " (country-name country)])))

(defn city-selector [cities]
  [:select {:field :list :id :city.id}
   (for [city (sort-by :name cities)]
     [:option {:key (:_id city)} (:name city)])])

(defn city-field []
  (println "Rendering city field for" (count @cities) "cities")
  (if-not @cities [:select [:option "Loading cities..."]]
    [(bind-fields (city-selector @cities) app-state)]))

(defn load-city-field [country-id]
  (when country-id
    (reset! cities nil)
    (reset! city-id nil)
    (println "Loading cities for" country-id)
    (GET (str "/api/cities/" country-id)
      {:handler #(reset! cities %)
       :response-format (json-response-format {:keywords? true})}))
  [city-field])

(defn forecast-chart []
  (println "Rendering forecast chart")
  [hc-chart {:config {}}])

(defn home-page []
  [:div [:h2 "Welcome to weather-demo"]
   [:div "You might want to check out the " [:a {:href "/weather"} "weather"]]])

(defn weather-page []
  (let [countries (atom nil)
        selected-country (atom nil)]
    (GET "/api/countries/"
      {:handler #(reset! countries %)
       :response-format (json-response-format {:keywords? true})})
    (fn []
      [:div [:h2 "Weather page"]
       [country-field @countries selected-country]
       [load-city-field (:id @selected-country)]
       [country-description @countries (:id @selected-country)]
       [forecast-chart]])))

(defn current-page []
  [:div [(session/get :current-page)]])

;; -------------------------
;; Routes

(secretary/defroute "/" []
  (session/put! :current-page #'home-page))

(secretary/defroute "/weather" []
  (session/put! :current-page #'weather-page))

;; -------------------------
;; Initialize app

(defn mount-root []
  (reagent/render [current-page] (.getElementById js/document "app")))

(defn init! []
  (accountant/configure-navigation!)
  (accountant/dispatch-current!)
  (mount-root))

(ns weather-demo.core
    (:require [reagent.core :as reagent :refer [atom]]
              [reagent.session :as session]
              [secretary.core :as secretary :include-macros true]
              [accountant.core :as accountant]

              [reagent-forms.core :refer [bind-fields]]
              [ajax.core :refer [GET json-response-format]]
              [clojure.string :as string]))

(def app-state (atom {}))
(def cities (reagent/cursor app-state [:cities]))
(def city-id (reagent/cursor app-state [:city :id]))

;; -------------------------
;; Components

(defn hc-chart-draw [this]
  (let [node (reagent/dom-node this)
        {:keys [config]} (reagent/props this)]
    (when config
      (js/Highcharts.Chart.
        (clj->js (assoc-in config [:chart :renderTo] node))))))

(defn hc-chart [config]
  (reagent/create-class {:display-name "highcharts"
                         :reagent-render (fn [] [:div])
                         :component-did-mount hc-chart-draw
                         :component-did-update hc-chart-draw}))

;; -------------------------
;; Views

(defn country-name [country]
  (let [name (-> country :name :common)
        region (-> country :region)]
    (string/join ", " (remove empty? [name region]))))

(defn country-selector [countries]
  [:select {:field :list :id :id}
   (for [country (sort-by country-name countries)]
     [:option {:key (:cca2 country)} (country-name country)])])

(defn country-field [countries country-atom]
  (println "Rendering country field for" (count countries) "countries")
  (if-not countries [:select [:option "Loading countries..."]]
    [bind-fields (country-selector countries) country-atom]))

(defn country-description [countries country-id]
  (println "Rendering country description for" country-id)
  (if-not country-id [:p "No country selected"]
    (let [country (first (filter #(= (:cca2 %) country-id) countries))]
      [:p "Selected country is " (country-name country)])))

(defn city-selector [cities]
  [:select {:field :list :id :city.id}
   (for [city (sort-by :name cities)]
     [:option {:key (:_id city)} (:name city)])])

(defn city-field []
  (println "Rendering city field for" (count @cities) "cities")
  (if-not @cities [:select [:option "Loading cities..."]]
    [(bind-fields (city-selector @cities) app-state)]))

(defn load-city-field [country-id]
  (when country-id
    (reset! cities nil)
    (reset! city-id nil)
    (println "Loading cities for" country-id)
    (GET (str "/api/cities/" country-id)
      {:handler #(reset! cities %)
       :response-format (json-response-format {:keywords? true})}))
  [city-field])

(defn forecast-chart [forecast-atom]
  (println "Rendering forecast chart for" (-> @forecast-atom :city :name))
  [hc-chart {:config {}}])

(defn load-forecast-chart [city-id]
  (let [forecast (atom nil)]
    (fn [city-id]
      (when city-id
        (reset! forecast nil)
        (println "Loading forecast data for city" city-id)
        (GET "data/2.5/forecast"
          {:params {:units "metric"
                    :id city-id}
           :handler #(reset! forecast %)
           :response-format (json-response-format {:keywords? true})}))
      [forecast-chart forecast])))

(defn home-page []
  [:div [:h2 "Welcome to weather-demo"]
   [:div "You might want to check out the " [:a {:href "/weather"} "weather"]]])

(defn weather-page []
  (let [countries (atom nil)
        selected-country (atom nil)]
    (GET "/api/countries/"
      {:handler #(reset! countries %)
       :response-format (json-response-format {:keywords? true})})
    (fn []
      [:div [:h2 "Weather page"]
       [country-field @countries selected-country]
       [load-city-field (:id @selected-country)]
       [country-description @countries (:id @selected-country)]
       [load-forecast-chart @city-id]])))

(defn current-page []
  [:div [(session/get :current-page)]])

;; -------------------------
;; Routes

(secretary/defroute "/" []
  (session/put! :current-page #'home-page))

(secretary/defroute "/weather" []
  (session/put! :current-page #'weather-page))

;; -------------------------
;; Initialize app

(defn mount-root []
  (reagent/render [current-page] (.getElementById js/document "app")))

(defn init! []
  (accountant/configure-navigation!)
  (accountant/dispatch-current!)
  (mount-root))

Load forecast data to be shown later with Highcharts.


(ns catalog.latex
  "Generate LaTeX for catalogues"
  (:require [catalog.vubis :as vubis]
            [clojure.java.shell :as shell]
            [clojure.string :as string]))

(def temp-name "/tmp/catalog.tex")
(def translations {:hörbuch "Hörbücher"
                   :braille "Braille"
                   :grossdruck "Grossdruck"
                   :e-book "E-Books"
                   :hörfilm "Hörfilme"
                   :ludo "Spiele"
                   :belletristik "Belletristik"
                   :sachbücher "Sachbücher"
                   :kinder-und-jugendbücher "Kinder- und Jugendbücher"
                   :action-und-thriller "Action und Thriller"
                   :beziehungsromane "Beziehungsromane"
                   :fantasy-science-fiction "Fantasy, Science Fiction"
                   :gesellschaftsromane "Gesellschaftsromane"
                   :historische-romane "Historische Romane"
                   :hörspiele "Hörspiele"
                   :krimis "Krimis"
                   :lebensgeschichten-und-schicksale "Lebensgeschichten und Schicksale"
                   :literarische-gattungen "Literarische Gattungen"
                   :literatur-in-fremdsprachen "Literatur in Fremdsprachen"
                   :mundart-heimat-natur "Mundart, Heimat, Natur"
                   :glaube-und-philosophie "Glaube und Philosophie"
                   :biografien "Biografien"
                   :freizeit-haus-garten "Freizeit, Haus, Garten"
                   :geschichte-und-gegenwart "Geschichte und Gegenwart"
                   :kunst-kultur-medien "Kunst, Kultur, Medien"
                   :lebensgestaltung-gesundheit-erziehung "Lebensgestaltung, Gesundheit, Erziehung"
                   :philosophie-religion-esoterik "Philosophie, Religion, Esoterik"
                   :reisen-natur-tiere "Reisen, Natur, Tiere"
                   :sprache "Sprache"
                   :wissenschaft-technik "Wissenschaft, Technik"
                   :jugendbücher "Jugendbücher"
                   :kinder-und-jugendsachbücher "Kinder- und Jugendsachbücher"
                   :kinderbücher-ab-10 "Kinderbücher (ab 10)"
                   :kinderbücher-ab-6 "Kinderbücher (ab 6)"})

(def formats [:hörbuch :braille :grossdruck :e-book :hörfilm :ludo])
(def genres [:belletristik :sachbücher :kinder-und-jugendbücher])
(def subgenres [:action-und-thriller :beziehungsromane :fantasy-science-fiction
                :gesellschaftsromane :historische-romane :hörspiele :krimis
                :lebensgeschichten-und-schicksale :literarische-gattungen
                :literatur-in-fremdsprachen :mundart-heimat-natur :glaube-und-philosophie
                :biografien :freizeit-haus-garten :geschichte-und-gegenwart
                :kunst-kultur-medien :lebensgestaltung-gesundheit-erziehung
                :philosophie-religion-esoterik :reisen-natur-tiere :sprache
                :wissenschaft-technik :jugendbücher :kinder-und-jugendsachbücher
                :kinderbücher-ab-10 :kinderbücher-ab-6])

(defn escape
  "Escape characters that have a special meaning to LaTeX (see [The
  Comprehensive LaTeX Symbol
  List](http://www.ctan.org/tex-archive/info/symbols/comprehensive/symbols-a4.pdf))"
  [text]
  (if (nil? text) ""
      (->
       text
       ;; backslash
       (string/replace #"\\" "\\\\textbackslash ")
       ;; drop excessive white space
       (string/replace #"\s+" " ")
       ;; quote special chars
       (string/replace #"(\$|&|%|#|_|\{|\})" "\\\\$1")
       ;; append a '{}' to special chars so they are not missinterpreted
       (string/replace #"(~|\^)" "$1{}")
       ;; add non-breaking space in front of emdash or endash followed by
       ;; punctuation
       (string/replace #" ([–—]\p{P})" " $1")
       ;; add non-breaking space in front ellipsis followed by punctuation
       (string/replace #" ((\.{3}|…)\p{P})" " $1")
       ;; [ and ] can sometimes be interpreted as the start or the end of
       ;; an optional argument
       (string/replace #"\[" "\\\\lbrack{}")
       (string/replace #"\]" "\\\\rbrack{}")
       ;; << and >> is apparently treated as a shorthand and is not
       ;; handled by our shorthand disabling code
       (string/replace #"<<" "{<}<")
       (string/replace #">>" "{>}>"))))

(defn preamble [{:keys [title font creator] :or {font "Verdana"}}]
  ["\\isopage[12]"
   "\\fixpdflayout"
   "\\checkandfixthelayout"
   "\\usepackage{graphicx}"
   "\\usepackage[ngerman]{babel}"
   "\\def\\languageshorthands#1{}"
   "\\usepackage{fontspec,xunicode,xltxtra}"
   "\\defaultfontfeatures{Mapping=tex-text}"
   (str "\\setmainfont{" font "}")
   "\\usepackage{hyperref}"
   (str "\\hypersetup{pdftitle={" (escape title) "}, pdfauthor={" (escape creator) "}}")
   "\\setsecnumdepth{subsubsection}"
   "\\setlength{\\parindent}{0pt}"])

(defn frontmatter [{:keys [title issue-number year]}]
  ["\\frontmatter"
   "\\begin{Spacing}{1.75}"
   (str  "{\\huge " (escape title) "}\\\\[0.5cm]")
   "\\end{Spacing}"
   "{\\large Ausgabenummer und Jahr}\\\\[1.5cm]"
   "\\vfill"
   "Logo\\\\ "
   "SBS Schweizerische Bibliothek für Blinde, Seh- und Lesebehinderte\\\\[0.5cm]"
   "\\cleartorecto"])

(defn mainmatter [env]
  ["\\mainmatter"
   ;; "\\pagestyle{plain}"
   "\\tableofcontents"])

(defn catalog-entry [{:keys [title creator description source_publisher source_date genre duration]}]
  ["\\begin{description}"
   (string/join " " [(format "\\item[%s]" (escape title))
                 (escape creator)
                 (escape source_publisher)
                 (escape source_date)
                 (translations genre) "\\\\"
                 (escape description)
                 "Spieldauer"
                 (escape duration)])
   "\\end{description}"])

(defn catalog-entries [items]
  (for [item items] (catalog-entry item)))

(defn subgenre-entry [subgenre items]
  (when (subgenre items)
    [(str "\\subsubsection{" (translations subgenre "FIXME")"}")
     (catalog-entries (subgenre items))]))

(defn subgenre-entries [items]
  (for [subgenre subgenres] (subgenre-entry subgenre items)))

(defn genre-entry [genre items]
  (when (genre items)
    [(str "\\subsection{" (translations genre "FIXME") "}")
     (subgenre-entries (genre items))]))

(defn genre-entries [items]
  (for [genre genres] (genre-entry genre items)))

(defn format-entry [format items]
  (when (format items)
    [(str "\\section{" (translations format "FIXME") "}")
     (genre-entries (format items))]))

(defn format-entries [items]
  (for [format formats] (format-entry format items)))

(defn catalog [{items :items}]
  ["\\chapter{Katalog}"
   (format-entries items)])

(defn impressum [env]
   ["\\chapter{Impressum}"
   ""
   "Neu im Sortiment"
   ""
   "Für Kundinnen und Kunden der SBS sowie für Interessenten"
   ""
   "Erscheint sechsmal jährlich und weist alle seit der letzten Ausgabe neu in die SBS aufgenommenen Bücher nach"
   ""
   "Neu im Sortiment kann im Jahresabonnement per Post zu CHF XX oder per E-Mail gratis bezogen werden."
   ""
   "Herausgeber:\\\\ "
   "SBS Schweizerische Bibliothek für Blinde, Seh- und Lesebehinderte\\\\ "
   "Grubenstrasse 12\\\\ "
   "CH-8045 Zürich\\\\ "
   "Fon +41 43 333 32 32\\\\ "
   "Fax +41 43 333 32 33\\\\ "
   "www.sbs.ch"
   ""
   "Abonnement, Ausleihe und Verkauf: nutzerservice@sbs.ch\\\\ "
   "Verkauf Institutionen: medienverlag@sbs.ch"
   ""
   "Copyright Vermerk"])

(defn document [{:keys [class options]
                 :or {class "memoir"
                      options #{"11pt" "a4paper" "oneside" "openright"}}
                 :as env}]
  (string/join "\n"
   (flatten
    [(str "\\documentclass[" (string/join "," options) "]{" class "}")
     (preamble env)
     "\\begin{document}"
     (frontmatter env)
     (mainmatter env)
     (catalog env)
     (impressum env)
     "\\end {document}"])))

(defn generate-latex []
  (let [items (->
               "/home/eglic/src/catalog/samples/vubis_export.xml"
               vubis/read-file
               vubis/order-and-group)]
    (spit temp-name (document
                     {:title "Neu im Sortiment"
                      :items items}))))

(defn generate-pdf []
  (shell/sh "latexmk" "-xelatex" temp-name :dir "/tmp"))

(ns catalog.latex
  "Generate LaTeX for catalogues"
  (:require [catalog.vubis :as vubis]
            [clojure.java.shell :as shell]
            [clojure.string :as string]))

(def temp-name "/tmp/catalog.tex")
(def translations {:hörbuch "Hörbücher"
                   :braille "Braille"
                   :grossdruck "Grossdruck"
                   :e-book "E-Books"
                   :hörfilm "Hörfilme"
                   :ludo "Spiele"
                   :belletristik "Belletristik"
                   :sachbücher "Sachbücher"
                   :kinder-und-jugendbücher "Kinder- und Jugendbücher"
                   :action-und-thriller "Action und Thriller"
                   :beziehungsromane "Beziehungsromane"
                   :fantasy-science-fiction "Fantasy, Science Fiction"
                   :gesellschaftsromane "Gesellschaftsromane"
                   :historische-romane "Historische Romane"
                   :hörspiele "Hörspiele"
                   :krimis "Krimis"
                   :lebensgeschichten-und-schicksale "Lebensgeschichten und Schicksale"
                   :literarische-gattungen "Literarische Gattungen"
                   :literatur-in-fremdsprachen "Literatur in Fremdsprachen"
                   :mundart-heimat-natur "Mundart, Heimat, Natur"
                   :glaube-und-philosophie "Glaube und Philosophie"
                   :biografien "Biografien"
                   :freizeit-haus-garten "Freizeit, Haus, Garten"
                   :geschichte-und-gegenwart "Geschichte und Gegenwart"
                   :kunst-kultur-medien "Kunst, Kultur, Medien"
                   :lebensgestaltung-gesundheit-erziehung "Lebensgestaltung, Gesundheit, Erziehung"
                   :philosophie-religion-esoterik "Philosophie, Religion, Esoterik"
                   :reisen-natur-tiere "Reisen, Natur, Tiere"
                   :sprache "Sprache"
                   :wissenschaft-technik "Wissenschaft, Technik"
                   :jugendbücher "Jugendbücher"
                   :kinder-und-jugendsachbücher "Kinder- und Jugendsachbücher"
                   :kinderbücher-ab-10 "Kinderbücher (ab 10)"
                   :kinderbücher-ab-6 "Kinderbücher (ab 6)"})

(def formats [:hörbuch :braille :grossdruck :e-book :hörfilm :ludo])
(def genres [:belletristik :sachbücher :kinder-und-jugendbücher])
(def subgenres [:action-und-thriller :beziehungsromane :fantasy-science-fiction
                :gesellschaftsromane :historische-romane :hörspiele :krimis
                :lebensgeschichten-und-schicksale :literarische-gattungen
                :literatur-in-fremdsprachen :mundart-heimat-natur :glaube-und-philosophie
                :biografien :freizeit-haus-garten :geschichte-und-gegenwart
                :kunst-kultur-medien :lebensgestaltung-gesundheit-erziehung
                :philosophie-religion-esoterik :reisen-natur-tiere :sprache
                :wissenschaft-technik :jugendbücher :kinder-und-jugendsachbücher
                :kinderbücher-ab-10 :kinderbücher-ab-6])

(defn escape
  "Escape characters that have a special meaning to LaTeX (see [The
  Comprehensive LaTeX Symbol
  List](http://www.ctan.org/tex-archive/info/symbols/comprehensive/symbols-a4.pdf))"
  [text]
  (if (nil? text) ""
      (->
       text
       ;; backslash
       (string/replace #"\\" "\\\\textbackslash ")
       ;; drop excessive white space
       (string/replace #"\s+" " ")
       ;; quote special chars
       (string/replace #"(\$|&|%|#|_|\{|\})" "\\\\$1")
       ;; append a '{}' to special chars so they are not missinterpreted
       (string/replace #"(~|\^)" "$1{}")
       ;; add non-breaking space in front of emdash or endash followed by
       ;; punctuation
       (string/replace #" ([–—]\p{P})" " $1")
       ;; add non-breaking space in front ellipsis followed by punctuation
       (string/replace #" ((\.{3}|…)\p{P})" " $1")
       ;; [ and ] can sometimes be interpreted as the start or the end of
       ;; an optional argument
       (string/replace #"\[" "\\\\lbrack{}")
       (string/replace #"\]" "\\\\rbrack{}")
       ;; << and >> is apparently treated as a shorthand and is not
       ;; handled by our shorthand disabling code
       (string/replace #"<<" "{<}<")
       (string/replace #">>" "{>}>"))))

(defn preamble [{:keys [title font creator] :or {font "Verdana"}}]
  ["\\isopage[12]"
   "\\fixpdflayout"
   "\\checkandfixthelayout"
   "\\usepackage{graphicx}"
   "\\usepackage[ngerman]{babel}"
   "\\def\\languageshorthands#1{}"
   "\\usepackage{fontspec,xunicode,xltxtra}"
   "\\defaultfontfeatures{Mapping=tex-text}"
   (str "\\setmainfont{" font "}")
   "\\usepackage{hyperref}"
   (str "\\hypersetup{pdftitle={" (escape title) "}, pdfauthor={" (escape creator) "}}")
   "\\setsecnumdepth{subsubsection}"
   "\\setlength{\\parindent}{0pt}"])

(defn frontmatter [{:keys [title issue-number year]}]
  ["\\frontmatter"
   "\\begin{Spacing}{1.75}"
   (str  "{\\huge " (escape title) "}\\\\[0.5cm]")
   "\\end{Spacing}"
   "{\\large Ausgabenummer und Jahr}\\\\[1.5cm]"
   "\\vfill"
   "Logo\\\\ "
   "SBS Schweizerische Bibliothek für Blinde, Seh- und Lesebehinderte\\\\[0.5cm]"
   "\\cleartorecto"])

(defn mainmatter [env]
  ["\\mainmatter"
   ;; "\\pagestyle{plain}"
   "\\tableofcontents"])

(defmulti catalog-entry (fn [{format :format}] format))

(defmethod catalog-entry :hörfilm
  [{:keys [title personel-name description source_publisher source_date genre library_signature]}]
  ["\\begin{description}"
   (string/join " "
                [(format "\\item[%s]" (escape title))
                 (format "Regie: %s" (escape personel-name))
                 (escape source_date)
                 (translations genre) "\\\\"
                 (escape description)
                 (escape library_signature)])
   "\\end{description}"])

(defmethod catalog-entry :hörbuch
  [{:keys [title creator description source_publisher source_date genre duration narrator
           producer producer_place produced_commercially
           library_signature]}]
  ["\\begin{description}"
   (string/join " "
                [(format "\\item[%s]" (escape title))
                 (escape creator)
                 (escape source_publisher)
                 (escape source_date)
                 (translations genre) "\\\\"
                 (escape description) "\\\\"
                 (escape duration)
                 (format "gelesen von: %s" (escape narrator))
                 (escape producer) (escape producer_place)
                 (escape produced_commercially)
                 (escape library_signature)])
   "\\end{description}"])

(defmethod catalog-entry :default
  [{:keys [title creator description source_publisher source_date genre duration]}]
  ["\\begin{description}"
   (string/join " "
                [(format "\\item[%s]" (escape title))
                 (escape creator)
                 (escape source_publisher)
                 (escape source_date)
                 (translations genre) "\\\\"
                 (escape description)
                 "Spieldauer"
                 (escape duration)])
   "\\end{description}"])

(defn catalog-entries [items]
  (for [item items] (catalog-entry item)))

(defn subgenre-entry [subgenre items]
  (when (subgenre items)
    [(str "\\subsubsection{" (translations subgenre "FIXME")"}")
     (catalog-entries (subgenre items))]))

(defn subgenre-entries [items]
  (for [subgenre subgenres] (subgenre-entry subgenre items)))

(defn genre-entry [genre items]
  (when (genre items)
    [(str "\\subsection{" (translations genre "FIXME") "}")
     (subgenre-entries (genre items))]))

(defn genre-entries [items]
  (for [genre genres] (genre-entry genre items)))

(defn format-entry [format items]
  (when (format items)
    [(str "\\section{" (translations format "FIXME") "}")
     (genre-entries (format items))]))

(defn format-entries [items]
  (for [format formats] (format-entry format items)))

(defn catalog [{items :items}]
  ["\\chapter{Katalog}"
   (format-entries items)])

(defn impressum [env]
   ["\\chapter{Impressum}"
   ""
   "Neu im Sortiment"
   ""
   "Für Kundinnen und Kunden der SBS sowie für Interessenten"
   ""
   "Erscheint sechsmal jährlich und weist alle seit der letzten Ausgabe neu in die SBS aufgenommenen Bücher nach"
   ""
   "Neu im Sortiment kann im Jahresabonnement per Post zu CHF XX oder per E-Mail gratis bezogen werden."
   ""
   "Herausgeber:\\\\ "
   "SBS Schweizerische Bibliothek für Blinde, Seh- und Lesebehinderte\\\\ "
   "Grubenstrasse 12\\\\ "
   "CH-8045 Zürich\\\\ "
   "Fon +41 43 333 32 32\\\\ "
   "Fax +41 43 333 32 33\\\\ "
   "www.sbs.ch"
   ""
   "Abonnement, Ausleihe und Verkauf: nutzerservice@sbs.ch\\\\ "
   "Verkauf Institutionen: medienverlag@sbs.ch"
   ""
   "Copyright Vermerk"])

(defn document [{:keys [class options]
                 :or {class "memoir"
                      options #{"11pt" "a4paper" "oneside" "openright"}}
                 :as env}]
  (string/join "\n"
   (flatten
    [(str "\\documentclass[" (string/join "," options) "]{" class "}")
     (preamble env)
     "\\begin{document}"
     (frontmatter env)
     (mainmatter env)
     (catalog env)
     (impressum env)
     "\\end {document}"])))

(defn generate-latex []
  (let [items (->
               "/home/eglic/src/catalog/samples/vubis_export.xml"
               vubis/read-file
               vubis/order-and-group)]
    (spit temp-name (document
                     {:title "Neu im Sortiment"
                      :items items}))))

(defn generate-pdf []
  (shell/sh "latexmk" "-xelatex" temp-name :dir "/tmp"))

Use multimethods for different catalog entries