(ns frontend.components.pieces.table
  (:require [devcards.core :as dc :refer-macros [defcard-om]]
            [om.core :as om :include-macros true])
  (:require-macros [frontend.utils :refer [component html]]))

(defn- cell-classes
  "The HTML classes applied to a cell (th or td) in a column of the given type."
  [type]
  (let [type (if (coll? type)
               type
               #{type})]
    ;; The types are implemented as classes which are named after the types.
    ;; This is an implementation detail.
    (into []
          (comp
           (filter #{:right :shrink})
           (map name))
          type)))

(defn table
  "Our standard table component.

  :columns - A sequence of column descriptions. Each is a map with the following keys:
             :header  - The content which should appear in the header cell of the column.
             :cell-fn - A function which, given a row object, returns the content for that
                        row's cell in this column.
             :type    - A column type, or a collection of types. Available types:
                        :right  - Column aligns its content to the right. Without this
                                  type, the column will align left.
                        :shrink - Column width shrinks to fit its content. Columns without
                                  :shrink will share any leftover space.
  :rows    - A sequence of objects which will each generate a row. These will be passed to
             the columns' :cell-fns to generate each cell.
  :key-fn  - A function of a row object which returns a value to use as the React
             key for the row.
  :striped - (optional) Adds a .striped class to the table."
  [{:keys [columns rows key-fn striped? ]} owner]
  {:pre (fn? key-fn)}
  (reify
    om/IRender
    (render [_]
      (component
        (html
         [:table
          [:thead {:class (when striped? "striped")}
           [:tr
            (for [[idx {:keys [header type]}] (map-indexed vector columns)]
              ;; We never reorder columns in a table, so the index works as a
              ;; React key. If we ever do reorder columns, we'll need to come up
              ;; with a key to identify them.
              [:th {:key idx
                    :class (cell-classes type)}
               header])]]
          [:tbody
           (for [row rows]
             [:tr (when key-fn {:key (key-fn row)})
              (for [[idx {:keys [cell-fn type]}] (map-indexed vector columns)]
                [:td {:key idx
                      :class (cell-classes type)}
                 (cell-fn row)])])]])))))

(defn action-button
  "A button suitable for the action button cell of a table row.

  label    - The textual label. Not visible; used as an aria-label.
  icon     - The icon rendered visually as the button.
  on-click - Handler called when the button is clicked."
  [label icon on-click]
  (html
   [:button {:data-component `action-button
             :aria-label label
             :on-click on-click}
    icon]))

(dc/do
  (defn format-date [date]
    (.toDateString date))

  (defn table-parent [data owner]
    (om/component
        (om/build table {:rows [{:name "John"
                                 :birthday (js/Date. "1940-10-09")}
                                {:name "Paul"
                                 :birthday (js/Date. "1942-06-18")}
                                {:name "George"
                                 :birthday (js/Date. "1943-02-25")}
                                {:name "Ringo"
                                 :birthday (js/Date. "1940-07-07")}]
                         :columns [{:header "Name"
                                    :cell-fn :name}
                                   {:header "Birthday"
                                    :cell-fn (comp format-date :birthday)}
                                   {:type :shrink
                                    :cell-fn (fn [beatle]
                                               (action-button
                                                "Remove"
                                                "X"
                                                #(js/alert (str "You may not remove " (:name beatle) " from the band."))))}]})))

  (defcard-om table
    table-parent))

(ns frontend.components.pieces.table
  (:require [devcards.core :as dc :refer-macros [defcard-om]]
            [om.core :as om :include-macros true])
  (:require-macros [frontend.utils :refer [component html]]))

(defn- cell-classes
  "The HTML classes applied to a cell (th or td) in a column of the given type."
  [type]
  (let [type (if (coll? type)
               type
               #{type})]
    ;; The types are implemented as classes which are named after the types.
    ;; This is an implementation detail.
    (into []
          (comp
           (filter #{:right :shrink})
           (map name))
          type)))

(defn table
  "Our standard table component.

  :columns - A sequence of column descriptions. Each is a map with the following keys:
             :header  - The content which should appear in the header cell of the column.
             :cell-fn - A function which, given a row object, returns the content for that
                        row's cell in this column.
             :type    - A column type, or a collection of types. Available types:
                        :right  - Column aligns its content to the right. Without this
                                  type, the column will align left.
                        :shrink - Column width shrinks to fit its content. Columns without
                                  :shrink will share any leftover space.
  :rows    - A sequence of objects which will each generate a row. These will be passed to
             the columns' :cell-fns to generate each cell.
  :key-fn  - A function of a row object which returns a value to use as the React
             key for the row.
  :striped? - (optional) Adds a .striped class to the table."
  [{:keys [columns rows key-fn striped? ]} owner]
  {:pre (fn? key-fn)}
  (reify
    om/IRender
    (render [_]
      (component
        (html
         [:table {:class (when striped? "striped")}
          [:thead
           [:tr
            (for [[idx {:keys [header type]}] (map-indexed vector columns)]
              ;; We never reorder columns in a table, so the index works as a
              ;; React key. If we ever do reorder columns, we'll need to come up
              ;; with a key to identify them.
              [:th {:key idx
                    :class (cell-classes type)}
               header])]]
          [:tbody
           (for [row rows]
             [:tr (when key-fn {:key (key-fn row)})
              (for [[idx {:keys [cell-fn type]}] (map-indexed vector columns)]
                [:td {:key idx
                      :class (cell-classes type)}
                 (cell-fn row)])])]])))))

(defn action-button
  "A button suitable for the action button cell of a table row.

  label    - The textual label. Not visible; used as an aria-label.
  icon     - The icon rendered visually as the button.
  on-click - Handler called when the button is clicked."
  [label icon on-click]
  (html
   [:button {:data-component `action-button
             :aria-label label
             :on-click on-click}
    icon]))

(dc/do
  (defn format-date [date]
    (.toDateString date))

  (defn table-parent [data owner]
    (om/component
        (om/build table {:key-fn :name
                         :rows [{:name "John"
                                 :birthday (js/Date. "1940-10-09")}
                                {:name "Paul"
                                 :birthday (js/Date. "1942-06-18")}
                                {:name "George"
                                 :birthday (js/Date. "1943-02-25")}
                                {:name "Ringo"
                                 :birthday (js/Date. "1940-07-07")}]
                         :columns [{:header "Name"
                                    :cell-fn :name}
                                   {:header "Birthday"
                                    :cell-fn (comp format-date :birthday)}
                                   {:type :shrink
                                    :cell-fn (fn [beatle]
                                               (action-button
                                                "Remove"
                                                "X"
                                                #(js/alert (str "You may not remove " (:name beatle) " from the band."))))}]})))

  (defcard-om table
    table-parent))

Fix docstring and key-fn devcards in tables


(ns lazytest.watch
  (:gen-class)
  (:use [lazytest.attach :only (all-groups)]
	[lazytest.plan :only (flat-plan)]
	[lazytest.run :only (run)]
	[lazytest.report :only (report)]
	[clojure.contrib.find-namespaces
	 :only (find-clojure-sources-in-dir
		read-file-ns-decl)]
	[clojure.java.io :only (file)]
	[clojure.string :only (split join)])
  (:import (java.util.concurrent ScheduledThreadPoolExecutor TimeUnit)
	   (java.util.regex Pattern)
	   (java.io File)))

(defn find-sources
  [dirs]
  {:pre [(coll? dirs)
	 (every? (fn [d] (instance? java.io.File d)) dirs)]}
  (mapcat find-clojure-sources-in-dir dirs))

(defn namespace-for-file [f]
  (second (read-file-ns-decl f)))

(defn newer-sources [dirs timestamp]
  (filter #(> (.lastModified %) timestamp) (find-sources dirs)))

(defn newer-namespaces [dirs timestamp]
  (remove nil? (map namespace-for-file (newer-sources dirs timestamp))))

(defn reload-and-run [dirs timestamp-atom reporter]
  (try
    (let [names (newer-namespaces dirs @timestamp-atom)]
      (when (seq names)
	(reset! timestamp-atom (System/currentTimeMillis))
	(println)
	(println "Reloading" (join ", " names))
	(doseq [n names] (remove-ns n))
	(doseq [n names] (require n :reload))
	(println "Running examples at" (java.util.Date.))
	(reporter (run (flat-plan (all-groups))))))
    (catch Exception e
      (println "ERROR:" e))))

(defn start [dirs & options]
  (let [dirs (map file dirs)
	{:keys [reporter delay], :or {delay 500, reporter report}} options
	last-run-timestamp (atom 0)
	runner #(reload-and-run dirs last-run-timestamp reporter)]
    (doto (ScheduledThreadPoolExecutor. 1)
      (.scheduleWithFixedDelay runner 0 delay TimeUnit/MILLISECONDS))))

(defn -main [& args]
  (println "Classpath contains the following:")
  (doseq [c (split (System/getProperty "java.class.path")
		   (Pattern/compile File/pathSeparator))]
    (println c))
  (start args))

(ns lazytest.watch
  (:gen-class)
  (:use [lazytest.attach :only (all-groups)]
	[lazytest.plan :only (default-plan)]
	[lazytest.run :only (run)]
	[lazytest.report :only (report)]
	[clojure.contrib.find-namespaces
	 :only (find-clojure-sources-in-dir
		read-file-ns-decl)]
	[clojure.java.io :only (file)]
	[clojure.string :only (split join)])
  (:import (java.util.concurrent ScheduledThreadPoolExecutor TimeUnit)
	   (java.util.regex Pattern)
	   (java.io File)))

(defn find-sources
  [dirs]
  {:pre [(coll? dirs)
	 (every? (fn [d] (instance? java.io.File d)) dirs)]}
  (mapcat find-clojure-sources-in-dir dirs))

(defn namespace-for-file [f]
  (second (read-file-ns-decl f)))

(defn newer-sources [dirs timestamp]
  (filter #(> (.lastModified %) timestamp) (find-sources dirs)))

(defn newer-namespaces [dirs timestamp]
  (remove nil? (map namespace-for-file (newer-sources dirs timestamp))))

(defn reload-and-run [dirs timestamp-atom reporter]
  (try
    (let [names (newer-namespaces dirs @timestamp-atom)]
      (when (seq names)
	(reset! timestamp-atom (System/currentTimeMillis))
	(println)
	(println "Reloading" (join ", " names))
	(doseq [n names] (remove-ns n))
	(doseq [n names] (require n :reload))
	(println "Running examples at" (java.util.Date.))
	(reporter (run (default-plan)))))
    (catch Exception e
      (println "ERROR:" e))))

(defn start [dirs & options]
  (let [dirs (map file dirs)
	{:keys [reporter delay], :or {delay 500, reporter report}} options
	last-run-timestamp (atom 0)
	runner #(reload-and-run dirs last-run-timestamp reporter)]
    (doto (ScheduledThreadPoolExecutor. 1)
      (.scheduleWithFixedDelay runner 0 delay TimeUnit/MILLISECONDS))))

(defn -main [& args]
  (println "Classpath contains the following:")
  (doseq [c (split (System/getProperty "java.class.path")
		   (Pattern/compile File/pathSeparator))]
    (println c))
  (start args))

Use default-plan in watch

Use default-plan in watch


(ns fulcro-template.server
  (:require
    [fulcro.server :as core]
    [com.stuartsierra.component :as component]

    [org.httpkit.server :refer [run-server]]
    [om.next.server :as om]
    [fulcro-template.api.read :as r]
    [fulcro-template.api.mutations :as mut]
    [om.next :refer [tree->db db->tree factory get-query]]
    [fulcro-template.api.user-db :as users]
    [om.dom :as dom]

    [fulcro-template.ui.root :as root]
    [fulcro-template.ui.html5-routing :as routing]
    [fulcro.client.core :as fc]

    [bidi.bidi :as bidi]
    [taoensso.timbre :as timbre]

    [ring.middleware.session :as session]
    [ring.middleware.session.store :as store]
    [ring.middleware.resource :as resource]
    [ring.middleware.content-type :refer [wrap-content-type]]
    [ring.middleware.gzip :refer [wrap-gzip]]
    [ring.middleware.not-modified :refer [wrap-not-modified]]
    [ring.middleware.params :refer [wrap-params]]
    [ring.middleware.resource :refer [wrap-resource]]
    [ring.middleware.cookies :as cookies]

    [ring.util.request :as req]
    [ring.util.response :as response]
    [fulcro.client.util :as util]
    [fulcro.server-render :as ssr]
    [fulcro-template.ui.user :as user]
    [clojure.string :as str]
    [fulcro.i18n :as i18n]
    [fulcro.client.mutations :as m]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER-SIDE RENDERING
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn top-html
  "Render the HTML for the SPA. There is only ever one kind of HTML to send, but the initial state and initial app view may vary.
  This function takes a normalized client database and a root UI class and generates that page."
  [normalized-client-state root-component-class]
  ; props are a "view" of the db. We use that to generate the view, but the initial state needs to be the entire db
  (let [props                (db->tree (get-query root-component-class) normalized-client-state normalized-client-state)
        root-factory         (factory root-component-class)
        app-html             (dom/render-to-str (root-factory props))
        initial-state-script (ssr/initial-state->script-tag normalized-client-state)]
    (str "<!DOCTYPE) html>\n"
      "\n"
      "\n"
      "<meta charset='UTF-8'>\n"
      "<meta name='viewport' content='width=device-width, initial-scale=1'>\n"
      "<link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet'>\n"
      initial-state-script
      "<title>Home Page (Dev Mode)</title>\n"
      "\n"
      "\n"
      "
"
      app-html
      "
\n"
      "\n"
      "\n"
      "\n")))

(defn build-app-state
  "Builds an up-to-date app state based on the URL where the db will contain everything needed. Returns a normalized
  client app db."
  [user uri bidi-match language]
  (let [base-state       (ssr/build-initial-state (fc/get-initial-state root/Root nil) root/Root) ; start with a normalized db that includes all union branches. Uses client UI!
        logged-in?       (boolean user)
        ; NOTE: All of these state functions are CLIENT code that we're leveraging on the server!
        set-route        (fn [s]
                           (if logged-in?
                             (fulcro.client.routing/update-routing-links s bidi-match)
                             (fulcro.client.routing/update-routing-links s {:handler :login})))
        set-user         (fn [s] (-> s
                                   (fc/merge-component user/User user)
                                   (assoc :logged-in? true :current-user (util/get-ident user/User user))))
        ; Augment the database with the detected route details and user.
        ; Also mark the app as ready, so the UI will render on the server, AND the client can detect that it was a server render.
        normalized-state (cond-> base-state
                           logged-in? set-user
                           (not logged-in?) (assoc :loaded-uri uri)
                           language (m/change-locale-impl language)
                           set-route (set-route)
                           :always (assoc :ui/ready? true))]
    normalized-state))

(defn render-page
  "Server-side render the entry page."
  [uri match user language]
  (let [normalized-app-state (build-app-state user uri match language)]
    (-> (top-html normalized-app-state root/Root)
      response/response
      (response/content-type "text/html"))))

(defn wrap-server-side-rendering
  "Ring middleware to handle all sends of the SPA page(s) via server-side rendering. If you want to see the client
  without SSR, just remove this component from the ring stack and supply an index.html in resources/public."
  [handler user-db]
  (fn [req]
    (let [uid         (some-> req :session :uid)            ; The UID is stored in server session store if they are logged in
          user        (users/get-user user-db uid)
          logged-in?  (boolean user)
          uri         (:uri req)
          bidi-match  (bidi/match-route routing/app-routes uri) ; where they were trying to go. NOTE: This is shared code with the client!
          valid-page? (boolean bidi-match)
          language    (some-> req :headers (get "accept-language") (str/split #",") first keyword)]

      ; . no valid bidi match. BYPASS. We don't handle it.
      (if valid-page?
        (render-page uri bidi-match user language)
        (handler req)))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; To handle the end of the processing chain.
(defn not-found [req]
  {:status  404
   :headers {"Content-Type" "text/plain"}
   :body    "Resource not found."})

; A place to put user's sessions. See Ring wrap-session.
; We could use Ring's in memory session store, but this one is a component that will reset on server restarts
(defrecord SessionStore [memory-store]
  store/SessionStore
  (read-session [_ key]
    (get @memory-store key))
  (write-session [_ key data]
    (let [key (or key (util/unique-key))]
      (swap! memory-store assoc key data)
      key))
  (delete-session [_ key]
    (swap! memory-store dissoc key)
    nil)
  component/Lifecycle
  (start [this] (assoc this :memory-store (atom {})))
  (stop [this] this))

; You need at least one of these. Specifies how to handle client requests, and participates in the component system
; of the server. Libraries can provide modules to install on your server to add functionality in a composable fashion.
(defrecord APIModule []
  core/Module
  (system-key [this] :api-module)                           ; this module will be known in the component system as :api-module. Allows you to inject the module.
  (components [this] {})                                    ; Additional components to build. This allows library modules to inject other dependencies that it needs into the system. Typically empty for applications.
  core/APIHandler
  (api-read [this] core/server-read)                        ; using fulcro multimethods so defmutation et al work.
  (api-mutate [this] core/server-mutate))

; A component that creates the full server middleware and stores it under the key :full-server-middleware (used by the web server).
; Your modules (e.g. APIModule above) are composed into one api-handler function by the fulcro-system function, which in
; turn is placed in the component system  under the ; key :fulcro.server/api-handler.
; The :fulcro.server/api-handler component in turn has a key :middleware whose value is the middleware for handling API requests from the client.
; So, you a component (CustomMiddleware) that composes *that* API middleware function
; into the larger whole. In our application our full-server-middleware needs the user
; database and session store (which are injected).
(defrecord CustomMiddleware [full-server-middleware api-handler session-store user-db]
  component/Lifecycle
  (stop [this] (dissoc this :full-server-middleware))
  (start [this]
    (let [wrap-api (:middleware api-handler)]
      ; The chained middleware function needs to be *stored* at :full-server-middleware,
      ; because we're using a Fulcro web server and it expects to find it there.
      (assoc this :full-server-middleware
                  (-> not-found
                    (wrap-resource "public")
                    (wrap-resource "cljsjs")
                    wrap-api                                ; from fulcro-system modules. Handles /api
                    (wrap-server-side-rendering user-db)
                    (session/wrap-session {:store session-store})
                    core/wrap-transit-params
                    core/wrap-transit-response
                    wrap-content-type
                    wrap-not-modified
                    wrap-params
                    wrap-gzip)))))

(def http-kit-opts
  [:ip :port :thread :worker-name-prefix
   :queue-size :max-body :max-line])

; A component that creates a web server and hooks lifecycle up to it. The server-middleware-component (CustomMiddleware)
; and config are injected.
(defrecord WebServer [config ^CustomMiddleware server-middleware-component server port]
  component/Lifecycle
  (start [this]
    (try
      (let [config         (:value config)                  ; config is a component, must pull out value
            server-opts    (select-keys config http-kit-opts)
            port           (:port server-opts)
            middleware     (:full-server-middleware server-middleware-component)
            started-server (run-server middleware server-opts)]
        (timbre/info (str "Web server (http://localhost:" port ")") "started successfully. Config of http-kit options:" server-opts)
        (assoc this :port port :server started-server))
      (catch Exception e
        (timbre/fatal "Failed to start web server " e)
        (throw e))))
  (stop [this]
    (if-not server this
                   (do (server)
                       (timbre/info "web server stopped.")
                       (assoc this :server nil)))))

; Injection configuration time! The Stuart Sierra component system handles all of the injection. You simple create
; components, place them into the system under a key, and wrap them with (component/using ...) in order to specify what they need.
; When the system is started, the dependencies will be analyzed and started in the correct order (leaves to tree top).
(defn make-system
  "Builds the server component system, which can then be started/stopped. `config-path` is a relative or absolute path
  to a web server configuration EDN file."
  [config-path]
  (core/fulcro-system
    {:components {:config                      (core/new-config config-path) ; you MUST use config if you use our server
                  ; Needed by Ring sessions. Composed in the Ring stack above
                  :session-store               (map->SessionStore {})
                  ; Creation/injection of the middleware stack
                  :server-middleware-component (component/using
                                                 (map->CustomMiddleware {})
                                                 ; the middleware needs the composed module's api handler, session store, and user database
                                                 {:api-handler   :fulcro.server/api-handler ; remap the generated api handler's key to api-handler, so it is easier to use there
                                                  :session-store :session-store
                                                  :user-db       :user-db})
                  ; The storage for user's who have signed up
                  :user-db                     (users/map->InMemoryUserDB {})
                  ; The web server itself, which needs the config and full-stack middleware.
                  :web-server                  (component/using (map->WebServer {})
                                                 [:config :server-middleware-component])}
     ; Modules are composable into the API handler (each can have their own read/mutate) and
     ; are joined together in a chain that is injected into the component system as :fulcro.server/api-handler
     :modules    [(component/using (map->APIModule {})
                    ; the things injected here will be available in the modules' parsing env
                    [:session-store :user-db])]}))

;; SEE: COMMIT 992d88d3005c6933c57c0416f557101507bd4681 in the Git History for an example using the Easy Server

(ns fulcro-template.server
  (:require
    [fulcro.server :as core]
    [com.stuartsierra.component :as component]

    [org.httpkit.server :refer [run-server]]
    [om.next.server :as om]
    [fulcro-template.api.read :as r]
    [fulcro-template.api.mutations :as mut]
    [om.next :refer [tree->db db->tree factory get-query]]
    [fulcro-template.api.user-db :as users]
    [om.dom :as dom]

    [fulcro-template.ui.root :as root]
    [fulcro-template.ui.html5-routing :as routing]
    [fulcro.client.core :as fc]

    [bidi.bidi :as bidi]
    [taoensso.timbre :as timbre]

    [ring.middleware.session :as session]
    [ring.middleware.session.store :as store]
    [ring.middleware.resource :as resource]
    [ring.middleware.content-type :refer [wrap-content-type]]
    [ring.middleware.gzip :refer [wrap-gzip]]
    [ring.middleware.not-modified :refer [wrap-not-modified]]
    [ring.middleware.params :refer [wrap-params]]
    [ring.middleware.resource :refer [wrap-resource]]
    [ring.middleware.cookies :as cookies]

    [ring.util.request :as req]
    [ring.util.response :as response]
    [fulcro.client.util :as util]
    [fulcro.server-render :as ssr]
    [fulcro-template.ui.user :as user]
    [clojure.string :as str]
    [fulcro.i18n :as i18n]
    [fulcro.client.mutations :as m]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER-SIDE RENDERING
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn top-html
  "Render the HTML for the SPA. There is only ever one kind of HTML to send, but the initial state and initial app view may vary.
  This function takes a normalized client database and a root UI class and generates that page."
  [normalized-client-state root-component-class]
  ; props are a "view" of the db. We use that to generate the view, but the initial state needs to be the entire db
  (let [props                (db->tree (get-query root-component-class) normalized-client-state normalized-client-state)
        root-factory         (factory root-component-class)
        app-html             (dom/render-to-str (root-factory props))
        initial-state-script (ssr/initial-state->script-tag normalized-client-state)]
    (str "<!DOCTYPE) html>\n"
      "\n"
      "\n"
      "<meta charset='UTF-8'>\n"
      "<meta name='viewport' content='width=device-width, initial-scale=1'>\n"
      "<link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet'>\n"
      initial-state-script
      "<title>Home Page (Dev Mode)</title>\n"
      "\n"
      "\n"
      "
"
      app-html
      "
\n"
      "\n"
      "\n"
      "\n")))

(defn build-app-state
  "Builds an up-to-date app state based on the URL where the db will contain everything needed. Returns a normalized
  client app db."
  [user uri bidi-match language]
  (let [base-state       (ssr/build-initial-state (fc/get-initial-state root/Root nil) root/Root) ; start with a normalized db that includes all union branches. Uses client UI!
        logged-in?       (boolean user)
        ; NOTE: All of these state functions are CLIENT code that we're leveraging on the server!
        set-route        (fn [s]
                           (if logged-in?
                             (fulcro.client.routing/update-routing-links s bidi-match)
                             (fulcro.client.routing/update-routing-links s {:handler :login})))
        set-user         (fn [s] (-> s
                                   (fc/merge-component user/User user)
                                   (assoc :logged-in? true :current-user (util/get-ident user/User user))))
        ; Augment the database with the detected route details and user.
        ; Also mark the app as ready, so the UI will render on the server, AND the client can detect that it was a server render.
        normalized-state (cond-> base-state
                           logged-in? set-user
                           (not logged-in?) (assoc :loaded-uri uri)
                           language (m/change-locale-impl language)
                           set-route (set-route)
                           :always (assoc :ui/ready? true))]
    normalized-state))

(defn render-page
  "Server-side render the entry page."
  [uri match user language]
  (let [normalized-app-state (build-app-state user uri match language)]
    (-> (top-html normalized-app-state root/Root)
      response/response
      (response/content-type "text/html"))))

(defn wrap-server-side-rendering
  "Ring middleware to handle all sends of the SPA page(s) via server-side rendering. If you want to see the client
  without SSR, just remove this component from the ring stack and supply an index.html in resources/public."
  [handler user-db]
  (fn [req]
    (let [uid         (some-> req :session :uid)            ; The UID is stored in server session store if they are logged in
          user        (users/get-user user-db uid)
          logged-in?  (boolean user)
          uri         (:uri req)
          bidi-match  (bidi/match-route routing/app-routes uri) ; where they were trying to go. NOTE: This is shared code with the client!
          valid-page? (boolean bidi-match)
          language    (some-> req :headers (get "accept-language") (str/split #",") first)]

      ; . no valid bidi match. BYPASS. We don't handle it.
      (if valid-page?
        (render-page uri bidi-match user language)
        (handler req)))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; To handle the end of the processing chain.
(defn not-found [req]
  {:status  404
   :headers {"Content-Type" "text/plain"}
   :body    "Resource not found."})

; A place to put user's sessions. See Ring wrap-session.
; We could use Ring's in memory session store, but this one is a component that will reset on server restarts
(defrecord SessionStore [memory-store]
  store/SessionStore
  (read-session [_ key]
    (get @memory-store key))
  (write-session [_ key data]
    (let [key (or key (util/unique-key))]
      (swap! memory-store assoc key data)
      key))
  (delete-session [_ key]
    (swap! memory-store dissoc key)
    nil)
  component/Lifecycle
  (start [this] (assoc this :memory-store (atom {})))
  (stop [this] this))

; You need at least one of these. Specifies how to handle client requests, and participates in the component system
; of the server. Libraries can provide modules to install on your server to add functionality in a composable fashion.
(defrecord APIModule []
  core/Module
  (system-key [this] :api-module)                           ; this module will be known in the component system as :api-module. Allows you to inject the module.
  (components [this] {})                                    ; Additional components to build. This allows library modules to inject other dependencies that it needs into the system. Typically empty for applications.
  core/APIHandler
  (api-read [this] core/server-read)                        ; using fulcro multimethods so defmutation et al work.
  (api-mutate [this] core/server-mutate))

; A component that creates the full server middleware and stores it under the key :full-server-middleware (used by the web server).
; Your modules (e.g. APIModule above) are composed into one api-handler function by the fulcro-system function, which in
; turn is placed in the component system  under the ; key :fulcro.server/api-handler.
; The :fulcro.server/api-handler component in turn has a key :middleware whose value is the middleware for handling API requests from the client.
; So, you a component (CustomMiddleware) that composes *that* API middleware function
; into the larger whole. In our application our full-server-middleware needs the user
; database and session store (which are injected).
(defrecord CustomMiddleware [full-server-middleware api-handler session-store user-db]
  component/Lifecycle
  (stop [this] (dissoc this :full-server-middleware))
  (start [this]
    (let [wrap-api (:middleware api-handler)]
      ; The chained middleware function needs to be *stored* at :full-server-middleware,
      ; because we're using a Fulcro web server and it expects to find it there.
      (assoc this :full-server-middleware
                  (-> not-found
                    (wrap-resource "public")
                    (wrap-resource "cljsjs")
                    wrap-api                                ; from fulcro-system modules. Handles /api
                    (wrap-server-side-rendering user-db)
                    (session/wrap-session {:store session-store})
                    core/wrap-transit-params
                    core/wrap-transit-response
                    wrap-content-type
                    wrap-not-modified
                    wrap-params
                    wrap-gzip)))))

(def http-kit-opts
  [:ip :port :thread :worker-name-prefix
   :queue-size :max-body :max-line])

; A component that creates a web server and hooks lifecycle up to it. The server-middleware-component (CustomMiddleware)
; and config are injected.
(defrecord WebServer [config ^CustomMiddleware server-middleware-component server port]
  component/Lifecycle
  (start [this]
    (try
      (let [config         (:value config)                  ; config is a component, must pull out value
            server-opts    (select-keys config http-kit-opts)
            port           (:port server-opts)
            middleware     (:full-server-middleware server-middleware-component)
            started-server (run-server middleware server-opts)]
        (timbre/info (str "Web server (http://localhost:" port ")") "started successfully. Config of http-kit options:" server-opts)
        (assoc this :port port :server started-server))
      (catch Exception e
        (timbre/fatal "Failed to start web server " e)
        (throw e))))
  (stop [this]
    (if-not server this
                   (do (server)
                       (timbre/info "web server stopped.")
                       (assoc this :server nil)))))

; Injection configuration time! The Stuart Sierra component system handles all of the injection. You simple create
; components, place them into the system under a key, and wrap them with (component/using ...) in order to specify what they need.
; When the system is started, the dependencies will be analyzed and started in the correct order (leaves to tree top).
(defn make-system
  "Builds the server component system, which can then be started/stopped. `config-path` is a relative or absolute path
  to a web server configuration EDN file."
  [config-path]
  (core/fulcro-system
    {:components {:config                      (core/new-config config-path) ; you MUST use config if you use our server
                  ; Needed by Ring sessions. Composed in the Ring stack above
                  :session-store               (map->SessionStore {})
                  ; Creation/injection of the middleware stack
                  :server-middleware-component (component/using
                                                 (map->CustomMiddleware {})
                                                 ; the middleware needs the composed module's api handler, session store, and user database
                                                 {:api-handler   :fulcro.server/api-handler ; remap the generated api handler's key to api-handler, so it is easier to use there
                                                  :session-store :session-store
                                                  :user-db       :user-db})
                  ; The storage for user's who have signed up
                  :user-db                     (users/map->InMemoryUserDB {})
                  ; The web server itself, which needs the config and full-stack middleware.
                  :web-server                  (component/using (map->WebServer {})
                                                 [:config :server-middleware-component])}
     ; Modules are composable into the API handler (each can have their own read/mutate) and
     ; are joined together in a chain that is injected into the component system as :fulcro.server/api-handler
     :modules    [(component/using (map->APIModule {})
                    ; the things injected here will be available in the modules' parsing env
                    [:session-store :user-db])]}))

;; SEE: COMMIT 992d88d3005c6933c57c0416f557101507bd4681 in the Git History for an example using the Easy Server

Fix locale value

Fix locale value

A new string? assertion introduced in https://github.com/fulcrologic/fulcro/commit/d025516779d4551fa390b9706309c8d0a57293c5 made this version of the template break.


(ns mdr2.production
  "Functionality for productions"
  (:refer-clojure :exclude [find])
  (:require [clojure.java.io :refer [file]]
            [me.raynes.fs :as fs]
            [clj-time.core :as t]
            [clj-time.format :as f]
            [environ.core :refer [env]]
            [mdr2.db :as db]
            [mdr2.state :as state]
            [mdr2.production.path :as path]
            [mdr2.obi :as obi]))

(def ^:private default-publisher "Swiss Library for the Blind, Visually Impaired and Print Disabled")
(def ^:private default-date-formatter (f/formatters :date))

(defn iso?
  "Return true if the production has an iso export"
  [production]
  (fs/exists? (path/iso-name production)))

(defn xml-path
  "Return the path to the meta data XML file, i.e. the DTBook file for a given production"
  [{id :id :as production}]
  (let [file-name (str id ".xml")
        path (path/structured-path production)]
    (.getPath (file path file-name))))

(defn manifest?
  "Return true if the production has a DAISY export"
  [production]
  (fs/exists? (path/manifest-path production)))

(defn dam-number
  "Return an id for a production as it is expected by legacy systems"
  [{id :id}]
  (str "dam" id))

(defn create
  "Create a production"
  [production]
  (as-> production p
        (add-default-meta-data p)
        (db/insert! p)
        (fs/mkdirs (path/structured-path p))))

(defn update-or-create!
  [production]
  (as-> production p
        (add-default-meta-data p)
        ;; if a production doesn't have an id yet, e.g. in the case of
        ;; a bulk import, the id is assigned in the db insert. For
        ;; that reason we need the as-> macro to thread the result of
        ;; the insert into the next function
        ;; FIXME: we should delay the insert into the db until the
        ;; dirs have been created, i.e. if anything the view fails the
        ;; db should be rolled back. Kinda like the
        ;; @transaction.commit_on_success annotation in Django
        ;; FIXME: shouldn't we update the DTBook XML when the meta
        ;; data is updated?
        (db/update-or-insert! p)
        (fs/mkdirs (path/structured-path p))))

(defn update! [production]
  (db/update! production))

(defn find
  "Find a production given its `id`"
  [id]
  (db/find id))

(defn find-all
  "Find all productions"
  []
  (db/find-all))

(defn find-by-productnumber
  "Find a production given its `product_number`"
  [product_number]
  (db/find-by-productnumber product_number))

(defn find-by-state
  "Find all productions with the given `state`"
  [state]
  (db/find-by-state state))

(defn delete-all-dirs
  "Delete all artifacts on the file system for a production"
  [production]
  (doseq [d [(path/structured-path production)
             (path/recording-path production)
             (path/recorded-path production)
             (path/encoded-path production)
             (path/iso-path production)]]
    fs/delete-dir d))

(defn delete
  "Delete a production with the given `id`"
  [id]
  (db/delete id)
  (delete-all-dirs {:id id}))

(defn uuid
  "Return a randomly generated UUID optionally prefixed with `prefix`"
  ([] (uuid "ch-sbs-"))
  ([prefix] (str prefix (java.util.UUID/randomUUID))))

(defn default-meta-data
  "Return default meta data"
  []
  {:publisher default-publisher
   :date (f/unparse default-date-formatter (t/now))
   :identifier (uuid)
   :language "de"
   :state state/initial-state})

(defn add-default-meta-data
  "Add the default meta data to a production"
  [production]
  (merge (default-meta-data) production))

(defn add-structure
  "Add a DTBook XML to a `production`. This will also set the status
  to :structured"
  [production f]
  ;; move the file to the right place
  (fs/move f (xml-path production))
  ;; create a config file for obi
  (obi/config-file production)
  ;; update the status
  (update! (assoc production :state :structured)))

(ns mdr2.production
  "Functionality for productions"
  (:refer-clojure :exclude [find])
  (:require [clojure.java.io :refer [file]]
            [me.raynes.fs :as fs]
            [clj-time.core :as t]
            [clj-time.format :as f]
            [environ.core :refer [env]]
            [mdr2.db :as db]
            [mdr2.state :as state]
            [mdr2.production.path :as path]
            [mdr2.obi :as obi])
  (:import java.nio.file.StandardCopyOption))

(def ^:private default-publisher "Swiss Library for the Blind, Visually Impaired and Print Disabled")
(def ^:private default-date-formatter (f/formatters :date))

(defn iso?
  "Return true if the production has an iso export"
  [production]
  (fs/exists? (path/iso-name production)))

(defn xml-path
  "Return the path to the meta data XML file, i.e. the DTBook file for a given production"
  [{id :id :as production}]
  (let [file-name (str id ".xml")
        path (path/structured-path production)]
    (.getPath (file path file-name))))

(defn manifest?
  "Return true if the production has a DAISY export"
  [production]
  (fs/exists? (path/manifest-path production)))

(defn dam-number
  "Return an id for a production as it is expected by legacy systems"
  [{id :id}]
  (str "dam" id))

(defn uuid
  "Return a randomly generated UUID optionally prefixed with `prefix`"
  ([] (uuid "ch-sbs-"))
  ([prefix] (str prefix (java.util.UUID/randomUUID))))

(defn default-meta-data
  "Return default meta data"
  []
  {:publisher default-publisher
   :date (f/unparse default-date-formatter (t/now))
   :identifier (uuid)
   :language "de"
   :state state/initial-state})

(defn add-default-meta-data
  "Add the default meta data to a production"
  [production]
  (merge (default-meta-data) production))

(defn create
  "Create a production"
  [production]
  (as-> production p
        (add-default-meta-data p)
        (db/insert! p)
        (fs/mkdirs (path/structured-path p))))

(defn update-or-create!
  [production]
  (as-> production p
        (add-default-meta-data p)
        ;; if a production doesn't have an id yet, e.g. in the case of
        ;; a bulk import, the id is assigned in the db insert. For
        ;; that reason we need the as-> macro to thread the result of
        ;; the insert into the next function
        ;; FIXME: we should delay the insert into the db until the
        ;; dirs have been created, i.e. if anything the view fails the
        ;; db should be rolled back. Kinda like the
        ;; @transaction.commit_on_success annotation in Django
        ;; FIXME: shouldn't we update the DTBook XML when the meta
        ;; data is updated?
        (db/update-or-insert! p)
        (fs/mkdirs (path/structured-path p))))

(defn update! [production]
  (db/update! production))

(defn find
  "Find a production given its `id`"
  [id]
  (db/find id))

(defn find-all
  "Find all productions"
  []
  (db/find-all))

(defn find-by-productnumber
  "Find a production given its `product_number`"
  [product_number]
  (db/find-by-productnumber product_number))

(defn find-by-state
  "Find all productions with the given `state`"
  [state]
  (db/find-by-state state))

(defn delete-all-dirs
  "Delete all artifacts on the file system for a production"
  [production]
  (doseq [d [(path/structured-path production)
             (path/recording-path production)
             (path/recorded-path production)
             (path/encoded-path production)
             (path/iso-path production)]]
    fs/delete-dir d))

(defn delete
  "Delete a production with the given `id`"
  [id]
  (db/delete id)
  (delete-all-dirs {:id id}))

(defn add-structure
  "Add a DTBook XML to a `production`. This will also set the status
  to :structured"
  [production f]
  ;; move the file to the right place
  (fs/move f (xml-path production) StandardCopyOption/REPLACE_EXISTING)
  ;; create a config file for obi
  (obi/config-file production)
  ;; update the status
  (update! (assoc production :state :structured)))

Make sure existing structure file is overwritten on new upload


(defproject patavi.server "0.2.1"
  :description "Clinici.co is a distributed system for exposing R as WAMP"
  :license {:name "The MIT License"
            :url "http://opensource.org/licenses/MIT"
            :distribution :repo}
  :url "http://patavi.com"
  :repositories {"sonatype-nexus-snapshots" "https://oss.sonatype.org/content/repositories/snapshots"
                 "sonatype-oss-public" "https://oss.sonatype.org/content/groups/public/" }
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [compojure "1.1.5"]
                 [patavi.common "0.1.0-SNAPSHOT"]
                 [ring/ring-devel "1.2.0"]
                 [http-kit "2.1.9"]
                 [clj-wamp "1.0.0"]
                 [overtone/at-at "1.2.0"]
                 [org.zeromq/cljzmq "0.1.1" :exclusions [org.zeromq/jzmq]]
                 [liberator "0.9.0"]]
  :profiles {:dev {:resource-paths ["resources-dev"]
                   :dependencies [[org.clojure/tools.namespace "0.2.4"]
                                  [org.jeromq/jeromq "0.3.0-SNAPSHOT"]]}
             :production {:dependencies [[org.zeromq/jzmq "2.2.2"]]
                          :resource-paths ["resources-prod"]
                          :jvm-opts ["-server" "-Djava.library.path=/usr/lib:/usr/local/lib"]}}
  :main patavi.server.server)

(defproject patavi.server "0.2.1"
  :description "Patavi is a distributed system for exposing R as WAMP"
  :license {:name "The MIT License"
            :url "http://opensource.org/licenses/MIT"
            :distribution :repo}
  :url "http://patavi.com"
  :repositories {"sonatype-nexus-snapshots" "https://oss.sonatype.org/content/repositories/snapshots"
                 "sonatype-oss-public" "https://oss.sonatype.org/content/groups/public/" }
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [compojure "1.1.5"]
                 [patavi.common "0.1.0-SNAPSHOT"]
                 [ring/ring-devel "1.2.0"]
                 [http-kit "2.1.9"]
                 [clj-wamp "1.0.0"]
                 [overtone/at-at "1.2.0"]
                 [org.zeromq/cljzmq "0.1.1" :exclusions [org.zeromq/jzmq]]
                 [liberator "0.9.0"]]
  :profiles {:dev {:resource-paths ["resources-dev"]
                   :dependencies [[org.clojure/tools.namespace "0.2.4"]
                                  [org.jeromq/jeromq "0.3.0-SNAPSHOT"]]}
             :production {:dependencies [[org.zeromq/jzmq "2.2.2"]]
                          :resource-paths ["resources-prod"]
                          :jvm-opts ["-server" "-Djava.library.path=/usr/lib:/usr/local/lib"]}}
  :main patavi.server.server)

Rename Clinici.co to Patavi

Rename Clinici.co to Patavi


;;; -*- Clojure -*- mode
(ns sledge.core
  (:require-macros [cljs.core.async.macros :refer [go alt!]])
  (:import [goog.net XhrIo])
  (:require [goog.events :as events]
            [clojure.string :as string]
            [cljs.core.async :as async :refer [>! <! put! chan]]
            [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true]
            ))

(enable-console-print!)

(defn detect-platform []
  (let [w (.-innerWidth js/window)]
    (condp > w
      480 :phone
      600 :tablet
      :desktop)))

(defn mobile? []
  (= (detect-platform) :phone))

(def app-state
  (atom
    {:results []
     :player-queue []
     :filters {}
     :device-type nil
     }))

(defn search-results []
  (om/ref-cursor (:results (om/root-cursor app-state))))

(defn player-queue []
  (om/ref-cursor (:player-queue (om/root-cursor app-state))))

(defn enqueue-track [track]
  (om/transact! (player-queue) #(conj % track)))

(defn dequeue-track [index]
  (om/transact! (player-queue)
                (fn [v] (vec (concat (subvec v 0 index)
                                     (subvec v (inc index)))))))

(defn dequeue-all []
  (om/transact! (player-queue) (fn [v] [])))

(defn mmss [seconds]
  (let [m (quot seconds 60)
        s (- seconds (* 60 m))]
    (str m ":" (.substr (str "000" s) -2))))

#_(println (mmss 20) (mmss 40) (mmss 60) (mmss 80)
         (mmss 200) (mmss 4000) (mmss 6000) (mmss 381))

(defn results-track-view [track owner]
  (reify
    om/IRender
    (render [this]
      (let [search-chan (om/get-shared owner :search-channel)
            artist (dom/span
                    #js {:className "artist"
                         :onClick #(put! search-chan
                                         {:artist (get @track "artist")})}
                    (get track "artist"))
            album (dom/span
                   #js {:className "album"
                        :onClick #(put! search-chan
                                        {:artist (get @track "artist")
                                         :album (get @track "album")})}
                   (get track "album"))
            title (dom/span #js {:className "title"}
                            (str (get track "track") " - " (get track "title")))
            duration (dom/span #js {:className "duration"} (mmss (get track "length")))
            button (dom/button #js {:onClick #(enqueue-track @track)} "+")]
        (apply dom/div #js {:className "track"}
               (if (mobile?)
                 [title artist album duration button]
                 [artist album title duration button]))))))

(defn xhr-search [term]
  (let [channel (chan)]
    (.send XhrIo "/tracks.json"
           (fn [e]
             (let [xhr (.-target e)
                   code (.getStatus xhr)
                   o (and (< code 400) (.getResponseJson xhr))
                   r (and o (js->clj o))]
               (put! channel (or r []))))
           "POST"
           (string/join
            " AND "
            (map (fn [[k v]] (str (name k) ": " (pr-str v))) term))
           {"Content-Type" "text/plain"}
           )
    channel))

(defn sorted-tracks [tracks]
  (sort-by
   #(vector (get % "artist")
            (get % "album")
            (js/parseInt (get % "track")))
   tracks))

(defn results-view [results owner]
  (reify
    om/IWillMount
    (will-mount [_]
      (let [channel (om/get-shared owner :search-channel)]
        (go (loop []
              (let [search-for (<! channel)
                    tracks (<! (xhr-search search-for))
                    sorted (sorted-tracks tracks)]
                (om/update! results (vec sorted))
                (recur))))))
    om/IRenderState
    (render-state [this {:keys [update-filters]}]
      (let [tracks (om/observe owner (search-results))
            button (dom/button
                    #js {:onClick
                         (fn [e] (doall (map #(enqueue-track %)
                                             tracks)))}
                    "+")
            track-components
            (om/build-all results-track-view tracks
                          {:init-state
                           {:update-filters update-filters }})]
        (if (mobile?)
          (apply dom/div #js {:className "results tracks" }
                 (dom/div #js {:className "track"}
                          (dom/span {:id "queue-all-tracks"}
                                    "Queue all tracks")
                          button)
                 track-components)
          (apply dom/div #js {:className "results tracks" }
                 (dom/div #js {:className "track header"}
                          (dom/span #js {:className "artist"} "Artist")
                          (dom/span #js {:className "album"} "Album" )
                          (dom/span #js {:className "title"} "Title")
                          (dom/span #js {:className "duration"} "Length")
                          button)
                 track-components))))
    ))

(defn queue-track-view [track owner]
  (reify
    om/IRenderState
    (render-state [this {:keys [index]}]
      (dom/div #js {:className "track"}
               (dom/span #js {:className "artist"} (get track "artist"))
               (dom/span #js {:className "album"} (get track "album" ))
               (dom/span #js {:className "title"} (get track "title"))
               (dom/span #js {:className "duration"} (mmss (get track "length")))
               (dom/button #js {:onClick #(dequeue-track index)}
                           "-")))))

(defn queue-view [app owner]
  (reify
    om/IRender
    (render [this]
      (let [queue (om/observe owner (player-queue))]
        (apply dom/div #js {:className "queue tracks"}
               (dom/div #js {:className "track header"}
                        (dom/span #js {:className "artist"} "Artist")
                        (dom/span #js {:className "album"} "Album" )
                        (dom/span #js {:className "title"} "Title")
                        (dom/span #js {:className "duration"} "Length")
                        (dom/button #js {:onClick #(dequeue-all)} "-"))
               (map #(om/build queue-track-view
                             %1
                             {:state {:index %2}})
                    queue (range 0 999))
               )))))

(defn filters-view [app owner]
  (reify
    om/IRenderState
    (render-state [this state]
      (let [search-chan (om/get-shared owner :search-channel)
            filters (:filters app)]
        (dom/div nil
                 (dom/h1
                  #js {:id "sledge"}
                  "sledge"
                  (dom/input #js {:ref "search-term"
                                  :id "search-term"
                                  :type "text"
                                  :placeholder "Search artist/album/title"
                                  :value (:search-term state)
                                  :onChange
                                  (fn [e]
                                    (let [term (.. e -target -value)]
                                      (om/set-state! owner :search-term term)
                                      (put! search-chan {:_content term})))
                                  }))
                 (apply dom/div #js {:className "filters" }
                        (map #(dom/span #js {:className "filter"
                                             :onClick
                                             (fn [e] (put! search-chan
                                                           {(first %) nil}))}
                                        (str (name (first %)) ": " (second  %)))
                             (filter second filters))))))))

(defn best-media-url [r]
  (let [urls (get r "_links")]
    (get
     (or (get urls "ogg") (get urls "mp3"))
     "href")))

(defn player-view [app owner]
  (reify
    om/IDidMount
    (did-mount [this]
      (let [el (om/get-node owner)]
        ;; last arg "true" is cos audio events don't bubble
        ;; http://stackoverflow.com/questions/11291651/why-dont-audio-and-video-events-bubble
        (.addEventListener el "ended" #(dequeue-track 0) true)))
    om/IRender
    (render [this]
      (let [queue (om/observe owner (player-queue))
        bits (best-media-url (first queue))]
        (dom/div nil
                 (dom/audio #js {:controls "controls"
                                 :autoPlay "true"
                                 :ref "player"
                                 :src bits
                                 })
                 )))))

(defn app-view [app owner]
  (reify
    om/IInitState
    (init-state [_]
      {:search-term ""
       :new-results (chan)
       :update-filters (chan)
       })
    om/IRenderState
    (render-state [this state]
      (dom/div nil
               (om/build filters-view app {:init-state state})
               (dom/h2 nil "results")
               (om/build results-view (:results app) {:init-state state})
               (dom/h2 nil "queue")
               (om/build queue-view app)
               (om/build player-view app)
               ))))

(defn init []
  (let [el (. js/document (getElementById "om-app"))
        search (chan)]
    (om/root app-view app-state
             {:target el
              :shared {:search-channel search}})))

(.addEventListener js/window "load" init)

;;; -*- Clojure -*- mode
(ns sledge.core
  (:require-macros [cljs.core.async.macros :refer [go alt!]])
  (:import [goog.net XhrIo])
  (:require [goog.events :as events]
            [clojure.string :as string]
            [cljs.core.async :as async :refer [>! <! put! chan]]
            [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true]
            ))

(enable-console-print!)

(defn detect-platform []
  (let [w (.-innerWidth js/window)]
    (condp > w
      480 :phone
      600 :tablet
      :desktop)))

(defn mobile? []
  (= (detect-platform) :phone))

(def app-state
  (atom
    {:results []
     :player-queue []
     :filters {}
     :device-type nil
     }))

(defn search-results []
  (om/ref-cursor (:results (om/root-cursor app-state))))

(defn player-queue []
  (om/ref-cursor (:player-queue (om/root-cursor app-state))))

(defn enqueue-track [track]
  (om/transact! (player-queue) #(conj % track)))

(defn dequeue-track [index]
  (om/transact! (player-queue)
                (fn [v] (vec (concat (subvec v 0 index)
                                     (subvec v (inc index)))))))

(defn dequeue-all []
  (om/transact! (player-queue) (fn [v] [])))

(defn mmss [seconds]
  (let [m (quot seconds 60)
        s (- seconds (* 60 m))]
    (str m ":" (.substr (str "000" s) -2))))

#_(println (mmss 20) (mmss 40) (mmss 60) (mmss 80)
         (mmss 200) (mmss 4000) (mmss 6000) (mmss 381))

(defn results-track-view [track owner]
  (reify
    om/IRender
    (render [this]
      (let [search-chan (om/get-shared owner :search-channel)
            artist (dom/span
                    #js {:className "artist"
                         :onClick #(put! search-chan
                                         {:artist (get @track "artist")})}
                    (get track "artist"))
            album (dom/span
                   #js {:className "album"
                        :onClick #(put! search-chan
                                        {:artist (get @track "artist")
                                         :album (get @track "album")})}
                   (get track "album"))
            title (dom/span #js {:className "title"}
                            (str (get track "track") " - " (get track "title")))
            duration (dom/span #js {:className "duration"} (mmss (get track "length")))
            button (dom/button #js {:onClick #(enqueue-track @track)} "+")]
        (apply dom/div #js {:className "track"}
               (if (mobile?)
                 [title artist album duration button]
                 [artist album title duration button]))))))

(defn xhr-search [term]
  (let [channel (chan)]
    (.send XhrIo "/tracks.json"
           (fn [e]
             (let [xhr (.-target e)
                   code (.getStatus xhr)
                   o (and (< code 400) (js->clj (.getResponseJson xhr)))]
               (put! channel (or o []))))
           "POST"
           (string/join
            " AND "
            (map (fn [[k v]] (str (name k) ": " (pr-str v))) term))
           {"Content-Type" "text/plain"}
           )
    channel))

(defn sorted-tracks [tracks]
  (sort-by
   #(vector (get % "artist")
            (get % "album")
            (js/parseInt (get % "track")))
   tracks))

(defn results-view [results owner]
  (reify
    om/IWillMount
    (will-mount [_]
      (let [channel (om/get-shared owner :search-channel)]
        (go (loop []
              (let [search-for (<! channel)
                    tracks (<! (xhr-search search-for))
                    sorted (sorted-tracks tracks)]
                (om/update! results (vec sorted))
                (recur))))))
    om/IRenderState
    (render-state [this {:keys [update-filters]}]
      (let [tracks (om/observe owner (search-results))
            button (dom/button
                    #js {:onClick
                         (fn [e] (doall (map #(enqueue-track %)
                                             tracks)))}
                    "+")
            track-components
            (om/build-all results-track-view tracks
                          {:init-state
                           {:update-filters update-filters }})]
        (if (mobile?)
          (apply dom/div #js {:className "results tracks" }
                 (dom/div #js {:className "track"}
                          (dom/span {:id "queue-all-tracks"}
                                    "Queue all tracks")
                          button)
                 track-components)
          (apply dom/div #js {:className "results tracks" }
                 (dom/div #js {:className "track header"}
                          (dom/span #js {:className "artist"} "Artist")
                          (dom/span #js {:className "album"} "Album" )
                          (dom/span #js {:className "title"} "Title")
                          (dom/span #js {:className "duration"} "Length")
                          button)
                 track-components))))
    ))

(defn queue-track-view [track owner]
  (reify
    om/IRenderState
    (render-state [this {:keys [index]}]
      (dom/div #js {:className "track"}
               (dom/span #js {:className "artist"} (get track "artist"))
               (dom/span #js {:className "album"} (get track "album" ))
               (dom/span #js {:className "title"} (get track "title"))
               (dom/span #js {:className "duration"} (mmss (get track "length")))
               (dom/button #js {:onClick #(dequeue-track index)}
                           "-")))))

(defn queue-view [app owner]
  (reify
    om/IRender
    (render [this]
      (let [queue (om/observe owner (player-queue))]
        (apply dom/div #js {:className "queue tracks"}
               (dom/div #js {:className "track header"}
                        (dom/span #js {:className "artist"} "Artist")
                        (dom/span #js {:className "album"} "Album" )
                        (dom/span #js {:className "title"} "Title")
                        (dom/span #js {:className "duration"} "Length")
                        (dom/button #js {:onClick #(dequeue-all)} "-"))
               (map #(om/build queue-track-view
                             %1
                             {:state {:index %2}})
                    queue (range 0 999))
               )))))

(defn filters-view [app owner]
  (reify
    om/IRenderState
    (render-state [this state]
      (let [search-chan (om/get-shared owner :search-channel)
            filters (:filters app)]
        (dom/div nil
                 (dom/h1
                  #js {:id "sledge"}
                  "sledge"
                  (dom/input #js {:ref "search-term"
                                  :id "search-term"
                                  :type "text"
                                  :placeholder "Search artist/album/title"
                                  :value (:search-term state)
                                  :onChange
                                  (fn [e]
                                    (let [term (.. e -target -value)]
                                      (om/set-state! owner :search-term term)
                                      (put! search-chan {:_content term})))
                                  }))
                 (apply dom/div #js {:className "filters" }
                        (map #(dom/span #js {:className "filter"
                                             :onClick
                                             (fn [e] (put! search-chan
                                                           {(first %) nil}))}
                                        (str (name (first %)) ": " (second  %)))
                             (filter second filters))))))))

(defn best-media-url [r]
  (let [urls (get r "_links")]
    (get
     (or (get urls "ogg") (get urls "mp3"))
     "href")))

(defn player-view [app owner]
  (reify
    om/IDidMount
    (did-mount [this]
      (let [el (om/get-node owner)]
        ;; last arg "true" is cos audio events don't bubble
        ;; http://stackoverflow.com/questions/11291651/why-dont-audio-and-video-events-bubble
        (.addEventListener el "ended" #(dequeue-track 0) true)))
    om/IRender
    (render [this]
      (let [queue (om/observe owner (player-queue))
        bits (best-media-url (first queue))]
        (dom/div nil
                 (dom/audio #js {:controls "controls"
                                 :autoPlay "true"
                                 :ref "player"
                                 :src bits
                                 })
                 )))))

(defn app-view [app owner]
  (reify
    om/IInitState
    (init-state [_]
      {:search-term ""
       :new-results (chan)
       :update-filters (chan)
       })
    om/IRenderState
    (render-state [this state]
      (dom/div nil
               (om/build filters-view app {:init-state state})
               (dom/h2 nil "results")
               (om/build results-view (:results app) {:init-state state})
               (dom/h2 nil "queue")
               (om/build queue-view app)
               (om/build player-view app)
               ))))

(defn init []
  (let [el (. js/document (getElementById "om-app"))
        search (chan)]
    (om/root app-view app-state
             {:target el
              :shared {:search-channel search}})))

(.addEventListener js/window "load" init)

simplify error checking code a bit

simplify error checking code a bit


(ns frontend.components.org-settings
  (:require [cljs.core.async :as async :refer [>! <! alts! chan sliding-buffer put! close!]]
            [frontend.routes :as routes]
            [frontend.datetime :as datetime]
            [frontend.models.organization :as org-model]
            [frontend.models.plan :as plan-model]
            [frontend.models.repo :as repo-model]
            [frontend.models.user :as user-model]
            [frontend.components.common :as common]
            [frontend.components.forms :as forms]
            [frontend.components.plans :as plans-component]
            [frontend.components.shared :as shared]
            [frontend.state :as state]
            [frontend.utils :as utils :include-macros true]
            [frontend.utils.github :as gh-utils]
            [frontend.utils.vcs-url :as vcs-url]
            [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true]
            [sablono.core :as html :refer-macros [html]]
            [clojure.string :as string]
            [goog.string :as gstring]
            [goog.string.format]
            [goog.style])
  (:require-macros [cljs.core.async.macros :as am :refer [go go-loop alt!]]
                   [dommy.macros :refer [node sel sel1]]))

(defn sidebar [{:keys [subpage plan org-name]} owner]
  (reify
    om/IRender
    (render [_]
      (letfn [(nav-links [templates]
                (map (fn [{:keys [page text]} msg]
                       [:li {:class (when (= page subpage) :active)}
                        [:a {:href (str "#" (name page))} text]])
                     templates))]
        (html [:div.span3
               [:ul.nav.nav-list.well
                [:li.nav-header "Organization settings"]
                [:li.divider]
                [:li.nav-header "Overview"]
                (nav-links [{:page :projects :text "Projects"}
                            {:page :users :text "Users"}])
                [:li.nav-header "Plan"]
                (when plan
                  (if (plan-model/can-edit-plan? plan org-name)
                    (nav-links [{:page :containers :text "Add containers"}
                                {:page :organizations :text "Organization"}
                                {:page :billing :text "Billing info"}
                                {:page :cancel :text "Cancel"}])
                    (nav-links [{:page :plan :text "Choose plan"}])))]])))))

(defn non-admin-plan [{:keys [org-name login]} owner]
  (reify
    om/IRender
    (render [_]
      (html [:div.row-fluid.plans
             [:div.span12
              [:h3
               "Do you want to create a plan for an organization that you don't admin?"]
              [:ol
               [:li
                "Sign up for a plan from your "
                [:a {:href (routes/v1-org-settings-subpage {:org-id login
                                                            :subpage "plan"})}
                 "\"personal organization\" page"]]
               [:li
                "Add " org-name
                " to the list of organizations you pay for or transfer the plan to "
                org-name " from the "
                [:a {:href (routes/v1-org-settings-subpage {:org-id login
                                                            :subpage "organizations"})}
                 "plan's organization page"]
                "."]]]]))))

(defn users [app owner]
  (reify
    om/IRender
    (render [_]
      (let [users (get-in app state/org-users-path)
            projects (get-in app state/org-projects-path)
            org-name (get-in app state/org-name-path)
            projects-by-follower (org-model/projects-by-follower projects)
            sorted-users (sort-by (fn [u]
                                    (- (count (get projects-by-follower (:login u)))))
                                  users)]
        (html
         [:div.users
          [:h2
           "CircleCI users in the " org-name " organization"]
          [:div
           (if-not (seq users)
             [:h4 "No users found."])
           [:div
            (for [user sorted-users
                  :let [login (:login user)
                        followed-projects (get projects-by-follower login)]]
              [:div.well.om-org-user
               {:class (if (zero? (count followed-projects))
                         "fail"
                         "success")}

               [:img.gravatar {:src (gh-utils/gravatar-url {:size 45 :login login
                                                            :github-id (:github_id user)
                                                            :gravatar_id (:gravatar_id user)})}]
               [:div.om-org-user-projects-container
                [:h4
                 (if (seq followed-projects)
                   (str login " is following:")
                   (str login " is not following any " org-name  " projects"))]
                [:div.om-org-user-projects
                 (for [project (sort-by (fn [p] (- (count (:followers p)))) followed-projects)
                       :let [vcs-url (:vcs_url project)]]
                   [:div.om-org-user-project
                    [:a {:href (routes/v1-project-dashboard {:org (vcs-url/org-name vcs-url)
                                                             :repo (vcs-url/repo-name vcs-url)})}
                     (vcs-url/project-name vcs-url)]])]]])]]])))))

(defn equalize-size
  "Given a node, will find all elements under node that satisfy selector and change
   the size of every element so that it is the same size as the largest element."
  [node selector]
  (let [items (sel node selector)
        sizes (map goog.style/getSize items)
        max-width (apply max (map #(.-width %) sizes))
        max-height (apply max (map #(.-height %) sizes))]
    (doseq [item items]
      (goog.style/setSize item max-width max-height))))

(defn followers-container [followers owner]
  (reify
    om/IDidMount
    (did-mount [_]
      (equalize-size (om/get-node owner) ".follower-container"))
    om/IDidUpdate
    (did-update [_ _ _]
      (equalize-size (om/get-node owner) ".follower-container"))
    om/IRender
    (render [_]
      (html
       [:div.followers-container.row-fluid
        [:div.row-fluid
         (for [follower followers]
           [:span.follower-container
            {:style {:display "inline-block"}}
            [:img.gravatar
             {:src (gh-utils/gravatar-url {:size 30 :login (:login follower)
                                           :gravatar_id (:gravatar_id follower)})}]
            " "
            [:span (:login follower)]])]]))))

(defn projects [app owner]
  (reify
    om/IRender
    (render [_]
      (let [users (get-in app state/org-users-path)
            projects (get-in app state/org-projects-path)
            {followed-projects true unfollowed-projects false} (group-by #(pos? (count (:followers %)))
                                                                         projects)
            org-name (get-in app state/org-name-path)]
        (html
         [:div
          [:div.followed-projects.row-fluid
           [:h2 "Followed projects"]
           (if-not (seq followed-projects)
             [:h4 "No followed projects found."]

             [:div.span8
              (for [project followed-projects
                    :let [vcs-url (:vcs_url project)]]
                [:div.row-fluid
                 [:div.span12.success.well
                  [:div.row-fluid
                   [:div.span12
                    [:h4
                     [:a {:href (routes/v1-project-dashboard {:org (vcs-url/org-name vcs-url)
                                                              :repo (vcs-url/repo-name vcs-url)})}
                      (vcs-url/project-name vcs-url)]
                     " "
                     [:a.edit-icon {:href (routes/v1-project-settings {:org (vcs-url/org-name vcs-url)
                                                                       :repo (vcs-url/repo-name vcs-url)})}
                      [:i.fa.fa-gear]]
                     " "
                     [:a.github-icon-link {:href vcs-url}
                      [:i.fa.fa-github]]]]]
                  (om/build followers-container (:followers project))]])])]
          [:div.row-fluid
           [:h2 "Untested repos"]
           (if-not (seq unfollowed-projects)
             [:h4 "No untested repos found."]

             [:div.span8
              (for [project unfollowed-projects
                    :let [vcs-url (:vcs_url project)]]
                [:div.row-fluid
                 [:div.fail.span12.well
                  [:h4
                   [:a {:href (routes/v1-project-dashboard {:org (vcs-url/org-name vcs-url)
                                                            :repo (vcs-url/repo-name vcs-url)})}
                    (vcs-url/project-name vcs-url)]
                   " "
                   [:a.edit-icon {:href (routes/v1-project-settings {:org (vcs-url/org-name vcs-url)
                                                                     :repo (vcs-url/repo-name vcs-url)})}
                    [:i.fa.fa-gear]]
                   " "
                   [:a.github-icon-link {:href vcs-url}
                    [:i.fa.fa-github]]]]])])]])))))

(defn plans-trial-notification [plan org-name controls-ch]
  [:div.row-fluid
   [:div.alert.alert-success {:class (when (plan-model/trial-over? plan) "alert-error")}
    [:p
     (if (plan-model/trial-over? plan)
       "Your 2-week trial is over!"

       [:span "The " [:strong org-name] " organization has "
        (plan-model/pretty-trial-time plan) " left in its trial."])]
    [:p
     "The trial plan is equivalent to the Solo plan with 6 containers."]
    (when (and (not (:too_many_extensions plan))
               (> 3 (plan-model/days-left-in-trial plan)))
      [:p
       "Need more time to decide? "
       (forms/stateful-button
        [:button.btn.btn-mini.btn-success
         {:data-success-text "Extended!",
          :data-loading-text "Extending...",
          :on-click #(put! controls-ch [:extend-trial {:org-name org-name}])}
         "Extend your trial"])])]])

(defn plans-piggieback-plan-notification [plan current-org-name]
  [:div.row-fluid
   [:div.offset1.span10
    [:div.alert.alert-success
     [:p
      "This organization is covered under " (:org_name plan) "'s plan which has "
      (:containers plan) " containers."]
     [:p
      "If you're an admin in the " (:org_name plan)
      " organization, then you can change plan settings from the "
      [:a {:href (routes/v1-org-settings-subpage {:org (:org_name plan)
                                                  :subpage "plan"})}
       (:org_name plan) " plan page"] "."]
     [:p
      "You can create a separate plan for " current-org-name " by selecting from the plans below."]]]])

(defn plan [app owner]
  (reify
    om/IRender
    (render [_]
      (let [plan (get-in data state/org-plan-path)
            org-name (get-in data state/org-name-path)
            controls-ch (om/get-shared owner [:comms :controls])]
        (html
         (if-not plan
           [:div.loading-spinner common/spinner]

           [:div#billing.plans.pricing.row-fluid
            (when (plan-model/trial? plan)
              (plans-trial-notification plan org-name controls-ch))
            (when (plan-model/piggieback? plan org-name)
              (plans-piggieback-plan-notification plan org-name))
            (om/build plans-component/plans app)
            (shared/customers-trust)
            plans-component/pricing-features
            plans-component/pricing-faq]))))))

(def main-component
  {:users users
   :projects projects
   :plan plan
   ;; :containers containers
   ;; :organizations organizations
   ;; :billing billing
   ;; :cancel cancel
   })

(defn org-settings [app owner]
  (reify
    om/IRender
    (render [_]
      (let [subpage (get app :org-settings-subpage)
            org-data (get-in app state/org-data-path)
            plan (get-in app state/org-plan-path)]
        (html [:div.container-fluid.org-page
               (if-not (:name org-data)
                 [:div.loading-spinner common/spinner]
                 [:div.row-fluid
                  (om/build sidebar {:subpage subpage :plan plan :org-name (:name org-data)})
                  [:div.span9
                   (common/flashes)
                   [:div#subpage
                    [:div
                     (if (:authorized? org-data)
                       (om/build (get main-component subpage projects) app)
                       [:div (om/build non-admin-plan
                                       {:login (get-in app [:current-user :login])
                                        :org-name (:org-settings-org-name app)
                                        :subpage subpage})])]]]])])))))

(ns frontend.components.org-settings
  (:require [cljs.core.async :as async :refer [>! <! alts! chan sliding-buffer put! close!]]
            [frontend.routes :as routes]
            [frontend.datetime :as datetime]
            [frontend.models.organization :as org-model]
            [frontend.models.plan :as plan-model]
            [frontend.models.repo :as repo-model]
            [frontend.models.user :as user-model]
            [frontend.components.common :as common]
            [frontend.components.forms :as forms]
            [frontend.components.plans :as plans-component]
            [frontend.components.shared :as shared]
            [frontend.state :as state]
            [frontend.stripe :as stripe]
            [frontend.utils :as utils :include-macros true]
            [frontend.utils.github :as gh-utils]
            [frontend.utils.vcs-url :as vcs-url]
            [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true]
            [sablono.core :as html :refer-macros [html]]
            [clojure.string :as string]
            [goog.string :as gstring]
            [goog.string.format]
            [goog.style])
  (:require-macros [cljs.core.async.macros :as am :refer [go go-loop alt!]]
                   [dommy.macros :refer [node sel sel1]]))

(defn sidebar [{:keys [subpage plan org-name]} owner]
  (reify
    om/IRender
    (render [_]
      (letfn [(nav-links [templates]
                (map (fn [{:keys [page text]} msg]
                       [:li {:class (when (= page subpage) :active)}
                        [:a {:href (str "#" (name page))} text]])
                     templates))]
        (html [:div.span3
               [:ul.nav.nav-list.well
                [:li.nav-header "Organization settings"]
                [:li.divider]
                [:li.nav-header "Overview"]
                (nav-links [{:page :projects :text "Projects"}
                            {:page :users :text "Users"}])
                [:li.nav-header "Plan"]
                (when plan
                  (if (plan-model/can-edit-plan? plan org-name)
                    (nav-links [{:page :containers :text "Add containers"}
                                {:page :organizations :text "Organization"}
                                {:page :billing :text "Billing info"}
                                {:page :cancel :text "Cancel"}])
                    (nav-links [{:page :plan :text "Choose plan"}])))]])))))

(defn non-admin-plan [{:keys [org-name login]} owner]
  (reify
    om/IRender
    (render [_]
      (html [:div.row-fluid.plans
             [:div.span12
              [:h3
               "Do you want to create a plan for an organization that you don't admin?"]
              [:ol
               [:li
                "Sign up for a plan from your "
                [:a {:href (routes/v1-org-settings-subpage {:org-id login
                                                            :subpage "plan"})}
                 "\"personal organization\" page"]]
               [:li
                "Add " org-name
                " to the list of organizations you pay for or transfer the plan to "
                org-name " from the "
                [:a {:href (routes/v1-org-settings-subpage {:org-id login
                                                            :subpage "organizations"})}
                 "plan's organization page"]
                "."]]]]))))

(defn users [app owner]
  (reify
    om/IRender
    (render [_]
      (let [users (get-in app state/org-users-path)
            projects (get-in app state/org-projects-path)
            org-name (get-in app state/org-name-path)
            projects-by-follower (org-model/projects-by-follower projects)
            sorted-users (sort-by (fn [u]
                                    (- (count (get projects-by-follower (:login u)))))
                                  users)]
        (html
         [:div.users
          [:h2
           "CircleCI users in the " org-name " organization"]
          [:div
           (if-not (seq users)
             [:h4 "No users found."])
           [:div
            (for [user sorted-users
                  :let [login (:login user)
                        followed-projects (get projects-by-follower login)]]
              [:div.well.om-org-user
               {:class (if (zero? (count followed-projects))
                         "fail"
                         "success")}

               [:img.gravatar {:src (gh-utils/gravatar-url {:size 45 :login login
                                                            :github-id (:github_id user)
                                                            :gravatar_id (:gravatar_id user)})}]
               [:div.om-org-user-projects-container
                [:h4
                 (if (seq followed-projects)
                   (str login " is following:")
                   (str login " is not following any " org-name  " projects"))]
                [:div.om-org-user-projects
                 (for [project (sort-by (fn [p] (- (count (:followers p)))) followed-projects)
                       :let [vcs-url (:vcs_url project)]]
                   [:div.om-org-user-project
                    [:a {:href (routes/v1-project-dashboard {:org (vcs-url/org-name vcs-url)
                                                             :repo (vcs-url/repo-name vcs-url)})}
                     (vcs-url/project-name vcs-url)]])]]])]]])))))

(defn equalize-size
  "Given a node, will find all elements under node that satisfy selector and change
   the size of every element so that it is the same size as the largest element."
  [node selector]
  (let [items (sel node selector)
        sizes (map goog.style/getSize items)
        max-width (apply max (map #(.-width %) sizes))
        max-height (apply max (map #(.-height %) sizes))]
    (doseq [item items]
      (goog.style/setSize item max-width max-height))))

(defn followers-container [followers owner]
  (reify
    om/IDidMount
    (did-mount [_]
      (equalize-size (om/get-node owner) ".follower-container"))
    om/IDidUpdate
    (did-update [_ _ _]
      (equalize-size (om/get-node owner) ".follower-container"))
    om/IRender
    (render [_]
      (html
       [:div.followers-container.row-fluid
        [:div.row-fluid
         (for [follower followers]
           [:span.follower-container
            {:style {:display "inline-block"}}
            [:img.gravatar
             {:src (gh-utils/gravatar-url {:size 30 :login (:login follower)
                                           :gravatar_id (:gravatar_id follower)})}]
            " "
            [:span (:login follower)]])]]))))

(defn projects [app owner]
  (reify
    om/IRender
    (render [_]
      (let [users (get-in app state/org-users-path)
            projects (get-in app state/org-projects-path)
            {followed-projects true unfollowed-projects false} (group-by #(pos? (count (:followers %)))
                                                                         projects)
            org-name (get-in app state/org-name-path)]
        (html
         [:div
          [:div.followed-projects.row-fluid
           [:h2 "Followed projects"]
           (if-not (seq followed-projects)
             [:h4 "No followed projects found."]

             [:div.span8
              (for [project followed-projects
                    :let [vcs-url (:vcs_url project)]]
                [:div.row-fluid
                 [:div.span12.success.well
                  [:div.row-fluid
                   [:div.span12
                    [:h4
                     [:a {:href (routes/v1-project-dashboard {:org (vcs-url/org-name vcs-url)
                                                              :repo (vcs-url/repo-name vcs-url)})}
                      (vcs-url/project-name vcs-url)]
                     " "
                     [:a.edit-icon {:href (routes/v1-project-settings {:org (vcs-url/org-name vcs-url)
                                                                       :repo (vcs-url/repo-name vcs-url)})}
                      [:i.fa.fa-gear]]
                     " "
                     [:a.github-icon-link {:href vcs-url}
                      [:i.fa.fa-github]]]]]
                  (om/build followers-container (:followers project))]])])]
          [:div.row-fluid
           [:h2 "Untested repos"]
           (if-not (seq unfollowed-projects)
             [:h4 "No untested repos found."]

             [:div.span8
              (for [project unfollowed-projects
                    :let [vcs-url (:vcs_url project)]]
                [:div.row-fluid
                 [:div.fail.span12.well
                  [:h4
                   [:a {:href (routes/v1-project-dashboard {:org (vcs-url/org-name vcs-url)
                                                            :repo (vcs-url/repo-name vcs-url)})}
                    (vcs-url/project-name vcs-url)]
                   " "
                   [:a.edit-icon {:href (routes/v1-project-settings {:org (vcs-url/org-name vcs-url)
                                                                     :repo (vcs-url/repo-name vcs-url)})}
                    [:i.fa.fa-gear]]
                   " "
                   [:a.github-icon-link {:href vcs-url}
                    [:i.fa.fa-github]]]]])])]])))))

(defn plans-trial-notification [plan org-name controls-ch]
  [:div.row-fluid
   [:div.alert.alert-success {:class (when (plan-model/trial-over? plan) "alert-error")}
    [:p
     (if (plan-model/trial-over? plan)
       "Your 2-week trial is over!"

       [:span "The " [:strong org-name] " organization has "
        (plan-model/pretty-trial-time plan) " left in its trial."])]
    [:p
     "The trial plan is equivalent to the Solo plan with 6 containers."]
    (when (and (not (:too_many_extensions plan))
               (> 3 (plan-model/days-left-in-trial plan)))
      [:p
       "Need more time to decide? "
       (forms/stateful-button
        [:button.btn.btn-mini.btn-success
         {:data-success-text "Extended!",
          :data-loading-text "Extending...",
          :on-click #(put! controls-ch [:extend-trial {:org-name org-name}])}
         "Extend your trial"])])]])

(defn plans-piggieback-plan-notification [plan current-org-name]
  [:div.row-fluid
   [:div.offset1.span10
    [:div.alert.alert-success
     [:p
      "This organization is covered under " (:org_name plan) "'s plan which has "
      (:containers plan) " containers."]
     [:p
      "If you're an admin in the " (:org_name plan)
      " organization, then you can change plan settings from the "
      [:a {:href (routes/v1-org-settings-subpage {:org (:org_name plan)
                                                  :subpage "plan"})}
       (:org_name plan) " plan page"] "."]
     [:p
      "You can create a separate plan for " current-org-name " by selecting from the plans below."]]]])

(defn plan [app owner]
  (reify

    ;; We're loading Checkout here because the loading status is not something
    ;; that we can hope to serialize into the state. It will be stale when we
    ;; move to a different browser or auto-refresh the page.
    ;; Making the component responsible for loading Checkout seems like the best
    ;; way to make sure it's loaded when we need it.
    om/IInitState
    (init-state [_]
      {:checkout-loaded? (stripe/checkout-loaded?)
       :checkout-loaded-chan (chan)})
    om/IWillMount
    (will-mount [_]
      (let [ch (om/get-state owner [:checkout-loaded-chan])
            checkout-loaded? (om/get-state owner [:checkout-loaded?])]
        (when-not checkout-loaded?
          (go (<! ch) ;; wait for success message
              (utils/mlog "Stripe checkout loaded")
              (om/set-state! owner [:checkout-loaded?] true))
          (utils/mlog "Loading Stripe checkout")
          (stripe/load-checkout ch))))
    om/IWillUnmount
    (will-unmount [_]
      (close! (om/get-state owner [:checkout-loaded-chan])))

    om/IRenderState
    (render-state [_ {:keys [checkout-loaded?]}]
      (let [plan (get-in app state/org-plan-path)
            org-name (get-in app state/org-name-path)
            controls-ch (om/get-shared owner [:comms :controls])]
        (html
         (if-not (and plan checkout-loaded?)
           [:div.loading-spinner common/spinner]

           [:div#billing.plans.pricing.row-fluid
            (when (plan-model/trial? plan)
              (plans-trial-notification plan org-name controls-ch))
            (when (plan-model/piggieback? plan org-name)
              (plans-piggieback-plan-notification plan org-name))
            (om/build plans-component/plans app)
            (shared/customers-trust)
            plans-component/pricing-features
            plans-component/pricing-faq]))))))

(def main-component
  {:users users
   :projects projects
   :plan plan
   ;; :containers containers
   ;; :organizations organizations
   ;; :billing billing
   ;; :cancel cancel
   })

(defn org-settings [app owner]
  (reify
    om/IRender
    (render [_]
      (let [subpage (get app :org-settings-subpage)
            org-data (get-in app state/org-data-path)
            plan (get-in app state/org-plan-path)]
        (html [:div.container-fluid.org-page
               (if-not (:name org-data)
                 [:div.loading-spinner common/spinner]
                 [:div.row-fluid
                  (om/build sidebar {:subpage subpage :plan plan :org-name (:name org-data)})
                  [:div.span9
                   (common/flashes)
                   [:div#subpage
                    [:div
                     (if (:authorized? org-data)
                       (om/build (get main-component subpage projects) app)
                       [:div (om/build non-admin-plan
                                       {:login (get-in app [:current-user :login])
                                        :org-name (:org-settings-org-name app)
                                        :subpage subpage})])]]]])])))))

make sure we've loaded checkout before we show users the plans