src/dmr/views.cljs
0674e354
 (ns dmr.views
8a16c975
   (:require
    [re-frame.core :as re-frame]
2771a4c3
    [clojure.string :as string]
2fdc7f54
    [goog.string :as gstr]
4d0e5d3f
    [dmr.config :as config]
0674e354
    [dmr.styles :as styles]
    [dmr.subs :as subs]
    [dmr.events :as events]))
8a16c975
 
2771a4c3
 ; TODO: move this functionto a separate 'utils' module
 (defn ordinal [n]
     (let [final-digit (mod n 10)
           suffix (case final-digit
                    0 "th"
                    1 "st"
                    2 "nd"
                    3 "rd"
                    "th")]
         (str n suffix)))
 
c9e74063
 (defn- join-lines [coll]
3f8355d8
     (string/join \newline coll))
2771a4c3
 
c9e74063
 (defn- comma-separated [coll]
250fac3a
     (string/join ", " coll))
 
c9e74063
 (defn- parenthesize [s]
250fac3a
     (when s (str "(" s ")")))
 
c9e74063
 (defn- spaced [& elements]
00d462f5
     (string/join " " (filter identity elements)))
 
80aaf0f7
 (defn- with-default [default x]
   (if (nil? x) default x))
 
 (defn- third [coll] (nth coll 2))
 
b83e55a8
 (defn- zip [& colls] (apply map vector colls))
 
 (defn- enumerated [& colls] (apply zip (range) colls))
 
a912156b
 
 (defn prompt []
   (let [cmdline (re-frame/subscribe [::subs/cmdline])
         suggestions (re-frame/subscribe [::subs/cmdline-suggestions])
         update-cmdline #(re-frame/dispatch-sync
                          [::events/update-cmdline (-> % .-target .-value)])
         key-pressed #(re-frame/dispatch-sync [::events/prompt-keypress %])
         submit-cmd #(do (.preventDefault %)
                         (re-frame/dispatch [::events/submit-cmd (-> % .-target)]))]
     [:form {:class (styles/prompt-style)
             :on-submit submit-cmd}
      [:input {:value @cmdline
               :on-change update-cmdline
               :on-keyDown key-pressed
               :type "text"}]
 
      [:ul.suggestions
       (let [{:keys [options selected]} @suggestions]
          (for [[idx x] (enumerated options)]
            ^{:key x} [:li
6faf0dc6
                       {:class (when (= idx selected) :selected)
d0b79894
                        :on-mouse-enter #(re-frame/dispatch [::events/hover-suggestion idx])
                        :on-click #(do (.preventDefault %)
                                       (re-frame/dispatch [::events/click-suggestion
                                                           (-> % .-target .-parentElement .-parentElement)
                                                           x]))}
a912156b
                       x]))]]))
 
 (defn title []
   (let [name (re-frame/subscribe [::subs/name])]
     [:h1 "Welcome to " @name]))
 
80aaf0f7
 (defn- property-list [& props]
2fdc7f54
     (into
      [:ul {:class (styles/property-list)}]
c33e1bff
      (for [[index [prop v]] (->> props (partition 2) (filter second) enumerated)]
        ^{:key index} [:li [:span prop] [:span v]])))
2fdc7f54
 
80aaf0f7
 
 (defn- property-table [items & props]
   (let [pprops (partition 3 props)
         headings (map first pprops)
         attrs (map second pprops)
         getters (map third pprops)]
     [:table {:class (styles/property-table)}
f4838048
       [:thead
e2b160d0
        [:tr
         (for [[index attr heading] (enumerated attrs headings)]
           ^{:key index}
            [:th attr heading])]]
80aaf0f7
       [:tbody
f4838048
        (for [[row-index item] (enumerated items)]
          ^{:key row-index}
           [:tr
            (for [[col-index attr f] (enumerated attrs getters)]
              ^{:key col-index}
               [:td attr (->> item f (with-default "-"))])])]]))
80aaf0f7
     
 (defn- license-notice [x]
557e889d
     (let [l (x :license-notice)]
       (when l
         [:section.license-notice
          (spaced (x :name) "is" (l :content-kind) "subject to the" (l :license-name))])))
 
0ab5b75f
 (defn- listing-license-notice [listing]
   [license-notice
    {:name "This listing"
     :license-notice (-> listing first :license-notice)}])
 
c9e74063
 (defn- description [x]
edb01dde
     (some->> x :desc (map #(vector :p %)) (into [:section.description])))
 
c9e74063
 (defn- heading [x]
bd1831f6
     [:h1 (get x :name (str x))])
edb01dde
 
3b4ee4ef
 (defn- pounds [w]
     (if (= w 1)
       (spaced w "lb")
       (spaced w "lbs")))
 
 (defn- cost [x]
     (when (x :cost)
         (spaced (-> x :cost :quantity) (-> x :cost :unit))))
 
 (defn- speed [x]
     (when (x :speed)
         (spaced (-> x :speed :quantity) (-> x :speed :unit))))
 
9a3f4b6d
 (defn- weight
     [x & {:keys [no-quantity unit] :or {no-quantity false unit pounds}}]
     (let [unit (if (string? unit) #(spaced % unit) unit)]
       (when (x :weight)
         (spaced
           (-> x :weight unit)
           (when (not no-quantity)
             (some->> x :quantity (str "for ") parenthesize))))))
edb01dde
 
c9e74063
 (defn- capacity [x]
     (some-> x :capacity))
 
8d09a4ec
 (defn- short-spell-description [spell]
   (spaced
     (-> spell :level ordinal)
     "level"
     (-> spell :school :name string/lower-case)
     "spell"))
 
99df709b
 (defn license-panel [license]
     [:article {:class (styles/license-panel)}
      [:h1 (license :license-name)]
      [:p (license :preamble)]
c9e74063
      [:ol.clauses
6268c11d
        (for [[index clause] (enumerated (license :clauses))]
           ^{:key index}
            [:li clause])]
      [:div.copyright-notice
0541dee0
        (for [[index text] (enumerated (license :copyright-notice))]
6268c11d
          ^{:key index} [:p text])]])
99df709b
 
3f8355d8
 (defn spell-panel [spell]
2771a4c3
       [:article {:class (styles/spell-panel)}
81aad3db
        [heading spell]
8d09a4ec
        [:div [:span.kind (short-spell-description spell)]]
81aad3db
        [property-list
2771a4c3
         ; Refactor these so that we get the "name" from the keyword by replacing
         ; '-' and '_' and capitalizing
00d462f5
          :Damage
            (let [level (-> spell :level str keyword)
                  damage (some-> spell :damage :damage_at_slot_level level)
                  damage-type (some-> spell :damage :damage_type :name string/lower-case)]
             (when damage (str damage ", " damage-type)))
          "Casting Time"
            (-> spell :casting_time)
          :Range
            (some-> spell :range string/lower-case (string/replace " feet" "'"))
          :Components
            (spaced
             (comma-separated (spell :components))
             (when (spell :material) (parenthesize (spell :material))))
          :Duration
            (spaced
             (-> spell :duration string/lower-case)
81aad3db
             (when (spell :concentration) "(requires concentration)"))]
        [description spell]
00d462f5
        (let [at-higher-levels (some-> spell :higher_level join-lines)]
         (when at-higher-levels
            [:section.higher-level
              [:span "At higher levels"]
f3f57785
              [:span at-higher-levels]]))
81aad3db
        [license-notice spell]])
a9e41bf1
 
2fdc7f54
 (defn equipment-panel [item]
     [:article {:class (styles/equipment-panel)}
81aad3db
      [heading item]
3b4ee4ef
      [:span.kind
       (spaced
        (-> item :equipment_category :name)
        (cond
          (item :gear_category) (-> item :gear_category :name string/lower-case parenthesize)
          (item :vehicle_category) (-> item :vehicle_category string/lower-case parenthesize)))]
81aad3db
      [property-list
3b4ee4ef
       :Speed
         (speed item)
c9e74063
       "Carrying Capacity"
         (capacity item)
3b4ee4ef
       :Weight
edb01dde
         (weight item)
3b4ee4ef
       :Cost
         (spaced
          (cost item)
81aad3db
          (some->> item :quantity (str "for ") parenthesize))]
      [description item]
      [license-notice item]])
2fdc7f54
 
 (defn weapon-panel [weapon]
  (let [weapon-kind (-> weapon :weapon_range string/lower-case)
        weapon-category (-> weapon :weapon_category string/lower-case)]
   [:article {:class (styles/weapon-panel)}
81aad3db
     [heading weapon]
edb01dde
     [:span.kind (spaced weapon-category weapon-kind "weapon")]
81aad3db
     [property-list
2fdc7f54
      ; 'two-handed-damage' is defined for *single-handed* weapons with the "versatile"
      ; property. Actual two-handed weapons have 'damage' only.
      :Damage
        (let [damage-type (-> weapon :damage :damage_type :name string/lower-case)
              damage (-> weapon :damage :damage_dice)
              two-handed-damage (some-> weapon :two_handed_damage :damage_dice)]
         (if two-handed-damage
3b4ee4ef
           (spaced damage (parenthesize two-handed-damage) damage-type)
           (spaced damage damage-type)))
2fdc7f54
      :Range
        (when (= weapon-kind "ranged")
          (gstr/format "%d' (%d')" (-> weapon :range :normal) (-> weapon :range :long)))
      :Properties
edb01dde
        (some->> weapon :properties (map :name) (map string/lower-case) comma-separated)
2fdc7f54
      :Weight
edb01dde
        (weight weapon)
2fdc7f54
      :Cost
81aad3db
        (cost weapon)]
     [license-notice weapon]]))
2fdc7f54
 
bd1831f6
 (defn spell-list-panel [spells]
   [:article {:class (styles/spell-list-panel)}
81aad3db
     [heading "Spells"]
bd1831f6
     [:ul
13f1ec66
      (for [[index spell] (enumerated (sort-by :name spells))]
         ^{:key index}
          [:li
            [:span (spell :name)]
0ab5b75f
            [:span (-> spell short-spell-description parenthesize)]])]
     [listing-license-notice spells]])
0179abce
 
80aaf0f7
 (defn equipment-list-panel [items]
   [:article {:class (styles/item-list-panel)}
81aad3db
    [heading "Items"]
    [property-table items
80aaf0f7
      :Name
        {}
        :name
      :Cost
        {:align :right}
        cost
      :Weight
        {:align :right}
0ab5b75f
        #(weight % :unit "lb" :no-quantity true)]
    [listing-license-notice items]])
0179abce
 
a9e41bf1
 (defn error-message
731380ac
     [{e :err}]
57bd4a3b
     [:div {:class (styles/error-message)} e])
a9e41bf1
 
c9e74063
 (defn input [x]
     [:div.input x])
a9e41bf1
 
99df709b
 (def is-license? :license-name)
731380ac
 (def is-spell? #(some-> % :url (string/starts-with? "/api/spells")))
406c6500
 (def is-spell-list? #(some-> % first is-spell?))
731380ac
 (def is-equipment? #(some-> % :url (string/starts-with? "/api/equipment")))
80aaf0f7
 (def is-equipment-list? #(some-> % first is-equipment?))
731380ac
 (def is-weapon? #(and (is-equipment? %) (-> % :equipment_category :index (= "weapon"))))
 (def is-error? #(contains? % :err))
 
 ; TODO: refactor this to avoid repetition of 'x'
a9e41bf1
 (defn output [x]
     [:div.output
      (cond
81aad3db
       (is-license? x) [license-panel x]
       (is-equipment-list? x) [equipment-list-panel x]
       (is-spell-list? x) [spell-list-panel x]
731380ac
       (is-spell? x) (spell-panel x)
34427c4a
       (is-equipment? x) (cond
81aad3db
                          (is-weapon? x) [weapon-panel x]
                          :else [equipment-panel x])
       (is-error? x) [error-message x])])
a9e41bf1
 
3f8355d8
 (defn history []
   (let [history (re-frame/subscribe [::subs/cmd-history])]
c9e74063
     [:ul {:class (styles/history-style)}
c4d9230a
       (for [[index item] (enumerated @history)]
81aad3db
         ^{:key index} [:li [input (item :input)] [output (item :output)]])]))
2771a4c3
 
13f07777
 (defn- link-to [href & content]
     [:a {:href href} content])
 
384aac16
 (def agpl-link
13f07777
     (link-to
      "https://www.gnu.org/licenses/agpl-3.0.html"
      "GNU Affero Public License"))
384aac16
 
0584fca2
 (def uberspace-link
     (link-to
      "https://uberspace.de/"
      "Uberspace"))
 
4d0e5d3f
 (defn footer []
   [:footer {:class (styles/footer)}
384aac16
    [:span.license agpl-link]
0584fca2
    [:span.version (spaced "version:" config/version)]
    [:span.host "Hosted by " uberspace-link]])
4d0e5d3f
 
64347ccd
 (defn main-panel []
c0e61749
   [:div {:class (styles/screen)}
00f7b042
    [:div {:class (styles/main-panel)}
4d0e5d3f
     [title] [history] [prompt] [footer]]])