<commit_before>(ns tourneybot-client.core
  (:require
    cljsjs.marked
    cljsjs.moment
    [clojure.string :refer [blank? lower-case]]
    [goog.functions :refer [once]]
    [tourneybot.data :refer [scheduled-status in-progress-status finished-status game-statuses
                             games->results
                             ensure-tournament-state
                             game-finished?]]
    [tourneybot.util :refer [atom-logger by-id js-log log fetch-ajax-text
                             fetch-json-as-cljs tourney-bot-url
                             game->date]]
    [rum.core :as rum]))

;;------------------------------------------------------------------------------
;; Constants
;;------------------------------------------------------------------------------

(def info-tab "INFO-TAB")
(def schedule-tab "SCHEDULE-TAB")
(def results-tab "RESULTS-TAB")
(def tab-values #{info-tab schedule-tab results-tab})

(def sort-on-name "SORT-BY-NAME")
(def sort-on-record "SORT-BY-RECORD")
(def sort-on-values #{sort-on-name sort-on-record})

(def tournament-state-url "tournament.json")
(def info-page-url "info.md")

;; TODO: allow this to be overriden with a query param
(def refresh-rate-ms 4000)

;;------------------------------------------------------------------------------
;; Page State Atom
;;------------------------------------------------------------------------------

(def initial-page-state
  {:hide-finished-games? false
   :schedule-search-text ""
   :sort-results-by sort-on-name
   :tab info-tab})

(def page-state (atom initial-page-state))

;; NOTE: useful for debugging
; (add-watch page-state :log atom-logger)

(defn- valid-page-state?
  "Some simple predicates to make sure the state is valid."
  [new-state]
  (and (map? new-state)
       (contains? tab-values (:tab new-state))
       (contains? sort-on-values (:sort-results-by new-state))))

(set-validator! page-state valid-page-state?)

;;------------------------------------------------------------------------------
;; Title
;;------------------------------------------------------------------------------

(defn- set-page-title [_kwd _the-atom old-state new-state]
  (let [old-title (:title old-state)
        new-title (:title new-state)]
    (when-not (= old-title new-title)
      (aset js/document "title" new-title))))

(add-watch page-state :title set-page-title)

;;------------------------------------------------------------------------------
;; Save UI-specific app state to localStorage
;;------------------------------------------------------------------------------

(def ls-key "client-page-state")

;; load any existing client state on startup
(try
  (let [state-string (js/window.localStorage.getItem ls-key)
        js-state (js/JSON.parse state-string)]
    (when (object? js-state)
      (let [clj-state (js->clj js-state :keywordize-keys true)]
        (swap! page-state merge clj-state))))
  (catch js/Error err nil))

(def ui-only-page-state-keys (keys initial-page-state))

(defn- save-client-state [_kwd _the-atom _old-state new-state]
  (let [ui-only-state (select-keys new-state ui-only-page-state-keys)
        js-state (clj->js ui-only-state)
        js-state-string (js/JSON.stringify js-state)]
    (try
      (js/window.localStorage.setItem ls-key js-state-string)
      (catch js/Error err nil))))

(add-watch page-state :client-state save-client-state)

;;------------------------------------------------------------------------------
;; Fetch Tournament State
;;------------------------------------------------------------------------------

(defn- fetch-tourney-state-success [new-state]
  ;; merge the tournament state with the page state
  (swap! page-state merge new-state))

(defn- fetch-tourney-state! []
  (fetch-json-as-cljs tournament-state-url fetch-tourney-state-success))

;; begin polling for udpates
(js/setInterval fetch-tourney-state! refresh-rate-ms)

;;------------------------------------------------------------------------------
;; Fetch Info Page
;;------------------------------------------------------------------------------

(def five-minutes (* 5 60 1000))
(def info-polling-ms five-minutes)

(defn- fetch-info-page-success [info-markdown]
  (let [info-html (js/marked info-markdown)
        info-el (by-id "infoContainer")]
    (when (and info-html info-el)
      (aset info-el "innerHTML" info-html))))

(defn- fetch-info-page! []
  (fetch-ajax-text info-page-url fetch-info-page-success))

;; poll for updates every 5 minutes
(js/setInterval fetch-info-page! info-polling-ms)

;;------------------------------------------------------------------------------
;; Results Page
;;------------------------------------------------------------------------------

(rum/defc ResultsTableHeader < rum/static
  [ties-allowed?]
  [:thead
    [:tr
      [:th {:style {:width "4%"}} ""]
      [:th {:style {:text-align "left"}} "Team"]
      [:th {:style {:width "30%"}}
        [:div.games-header "Games"]
        [:div.points-header "Points"]]]])

(rum/defc TeamNameCell < rum/static
  [team-name captain]
  [:div.team-name-container
    [:div.team-name team-name]
    (when (and (string? captain)
               (not (blank? captain)))
      [:div.captain captain])])

(rum/defc RecordCell < rum/static
  [ties-allowed? {:keys [games-won games-lost games-tied
                         points-won points-lost points-diff]}]
  [:td.record-cell
    [:div.big-record
      [:span.big-num games-won]
      [:span.dash "-"]
      [:span.big-num games-lost]
      (when ties-allowed?
        (list [:span.dash "-"]
              [:span.big-num games-tied]))]
    [:div.small-points
      [:span.small-point (str "+" points-won)]
      [:span ", "]
      [:span.small-point (str "-" points-lost)]
      [:span ", "]
      [:span.small-point (if (neg? points-diff)
                           points-diff
                           (str "+" points-diff))]]])

(rum/defc ResultRow < rum/static
  [ties-allowed? result]
  (let [no-games-played? (zero? (:games-played result))]
    [:tr
      [:td.place (when-not no-games-played? (str "#" (:place result)))]
      [:td.team-name (TeamNameCell (:team-name result) (:team-captain result))]
      (if (and (zero? (:games-won result))
               (zero? (:games-lost result)))
        [:td.record-cell.no-games "-"]
        (RecordCell ties-allowed? result))]))

(defn- click-sort-results-link [mode js-evt]
  (.preventDefault js-evt)
  (swap! page-state assoc :sort-results-by mode))

(rum/defc SortByToggle < rum/static
  [mode]
  [:div.sort-by-container
    [:label "Sort by:"]
    [:a {:class (str "left" (when (= mode sort-on-name) " active"))
         :href "#"
         :on-click (partial click-sort-results-link sort-on-name)
         :on-touch-start (partial click-sort-results-link sort-on-name)}
      "Team Name"]
    [:a {:class (str "right" (when (= mode sort-on-record) " active"))<commit_msg>fix a bug on the results page when there are ties<commit_after>(ns tourneybot-client.core
  (:require
    cljsjs.marked
    cljsjs.moment
    [clojure.string :refer [blank? lower-case]]
    [goog.functions :refer [once]]
    [tourneybot.data :refer [scheduled-status in-progress-status finished-status game-statuses
                             games->results
                             ensure-tournament-state
                             game-finished?]]
    [tourneybot.util :refer [atom-logger by-id js-log log fetch-ajax-text
                             fetch-json-as-cljs tourney-bot-url
                             game->date]]
    [rum.core :as rum]))

;;------------------------------------------------------------------------------
;; Constants
;;------------------------------------------------------------------------------

(def info-tab "INFO-TAB")
(def schedule-tab "SCHEDULE-TAB")
(def results-tab "RESULTS-TAB")
(def tab-values #{info-tab schedule-tab results-tab})

(def sort-on-name "SORT-BY-NAME")
(def sort-on-record "SORT-BY-RECORD")
(def sort-on-values #{sort-on-name sort-on-record})

(def tournament-state-url "tournament.json")
(def info-page-url "info.md")

;; TODO: allow this to be overriden with a query param
(def refresh-rate-ms 4000)

;;------------------------------------------------------------------------------
;; Page State Atom
;;------------------------------------------------------------------------------

(def initial-page-state
  {:hide-finished-games? false
   :schedule-search-text ""
   :sort-results-by sort-on-name
   :tab info-tab})

(def page-state (atom initial-page-state))

;; NOTE: useful for debugging
; (add-watch page-state :log atom-logger)

(defn- valid-page-state?
  "Some simple predicates to make sure the state is valid."
  [new-state]
  (and (map? new-state)
       (contains? tab-values (:tab new-state))
       (contains? sort-on-values (:sort-results-by new-state))))

(set-validator! page-state valid-page-state?)

;;------------------------------------------------------------------------------
;; Title
;;------------------------------------------------------------------------------

(defn- set-page-title [_kwd _the-atom old-state new-state]
  (let [old-title (:title old-state)
        new-title (:title new-state)]
    (when-not (= old-title new-title)
      (aset js/document "title" new-title))))

(add-watch page-state :title set-page-title)

;;------------------------------------------------------------------------------
;; Save UI-specific app state to localStorage
;;------------------------------------------------------------------------------

(def ls-key "client-page-state")

;; load any existing client state on startup
(try
  (let [state-string (js/window.localStorage.getItem ls-key)
        js-state (js/JSON.parse state-string)]
    (when (object? js-state)
      (let [clj-state (js->clj js-state :keywordize-keys true)]
        (swap! page-state merge clj-state))))
  (catch js/Error err nil))

(def ui-only-page-state-keys (keys initial-page-state))

(defn- save-client-state [_kwd _the-atom _old-state new-state]
  (let [ui-only-state (select-keys new-state ui-only-page-state-keys)
        js-state (clj->js ui-only-state)
        js-state-string (js/JSON.stringify js-state)]
    (try
      (js/window.localStorage.setItem ls-key js-state-string)
      (catch js/Error err nil))))

(add-watch page-state :client-state save-client-state)

;;------------------------------------------------------------------------------
;; Fetch Tournament State
;;------------------------------------------------------------------------------

(defn- fetch-tourney-state-success [new-state]
  ;; merge the tournament state with the page state
  (swap! page-state merge new-state))

(defn- fetch-tourney-state! []
  (fetch-json-as-cljs tournament-state-url fetch-tourney-state-success))

;; begin polling for udpates
(js/setInterval fetch-tourney-state! refresh-rate-ms)

;;------------------------------------------------------------------------------
;; Fetch Info Page
;;------------------------------------------------------------------------------

(def five-minutes (* 5 60 1000))
(def info-polling-ms five-minutes)

(defn- fetch-info-page-success [info-markdown]
  (let [info-html (js/marked info-markdown)
        info-el (by-id "infoContainer")]
    (when (and info-html info-el)
      (aset info-el "innerHTML" info-html))))

(defn- fetch-info-page! []
  (fetch-ajax-text info-page-url fetch-info-page-success))

;; poll for updates every 5 minutes
(js/setInterval fetch-info-page! info-polling-ms)

;;------------------------------------------------------------------------------
;; Results Page
;;------------------------------------------------------------------------------

(rum/defc ResultsTableHeader < rum/static
  [ties-allowed?]
  [:thead
    [:tr
      [:th {:style {:width "4%"}} ""]
      [:th {:style {:text-align "left"}} "Team"]
      [:th {:style {:width "30%"}}
        [:div.games-header "Games"]
        [:div.points-header "Points"]]]])

(rum/defc TeamNameCell < rum/static
  [team-name captain]
  [:div.team-name-container
    [:div.team-name team-name]
    (when (and (string? captain)
               (not (blank? captain)))
      [:div.captain captain])])

(rum/defc RecordCell < rum/static
  [ties-allowed? {:keys [games-won games-lost games-tied
                         points-won points-lost points-diff]}]
  [:td.record-cell
    [:div.big-record
      [:span.big-num games-won]
      [:span.dash "-"]
      [:span.big-num games-lost]
      (when ties-allowed?
        (list [:span.dash "-"]
              [:span.big-num games-tied]))]
    [:div.small-points
      [:span.small-point (str "+" points-won)]
      [:span ", "]
      [:span.small-point (str "-" points-lost)]
      [:span ", "]
      [:span.small-point (if (neg? points-diff)
                           points-diff
                           (str "+" points-diff))]]])

(rum/defc ResultRow < rum/static
  [ties-allowed? result]
  (let [no-games-played? (zero? (:games-played result))]
    [:tr
      [:td.place (when-not no-games-played? (str "#" (:place result)))]
      [:td.team-name (TeamNameCell (:team-name result) (:team-captain result))]
      (if no-games-played?
        [:td.record-cell.no-games "-"]
        (RecordCell ties-allowed? result))]))

(defn- click-sort-results-link [mode js-evt]
  (.preventDefault js-evt)
  (swap! page-state assoc :sort-results-by mode))

(rum/defc SortByToggle < rum/static
  [mode]
  [:div.sort-by-container
    [:label "Sort by:"]
    [:a {:class (str "left" (when (= mode sort-on-name) " active"))
         :href "#"
         :on-click (partial click-sort-results-link sort-on-name)
         :on-touch-start (partial click-sort-results-link sort-on-name)}
      "Team Name"]
    [:a {:class (str "right" (when (= mode sort-on-record) " active"))
{'fraction_non_alphanumeric': 0.29846957278595776, 'fraction_numerical': 0.0021759628635671284, 'mean_word_length': 1.5946556266465939, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3091406', 'n_tokens_mistral': 4188, 'n_tokens_neox': 3976, 'n_words': 931}