I’m working on implementing a spelling bee game with CLJS, re-frame, and shadow-cljs. Here is my code:

(ns spelling-bee.core
(:require

[clojure.set :as set]
[clojure.string :as str]
[re-frame.core :as rf]
[reagent.dom :as rdom]
[stylefy.core :as stylefy :refer [use-style]]
[stylefy.reagent :as stylefy-reagent]))

(def debug?
^boolean goog.DEBUG)


;---------- our app state atom ----------

(def default-db
{:name “player”
:game-started false
:words #{}
:common-letter #{}
:letters #{}
:display-letters []
:found-words #{}
:current-input “”
:message “Welcome to the Spelling Bee!”
:score 0})



;---------- various functions ----------

;; Later this can be substituted with a database call to pull a list of words.
(def word-collection #{“babyproof” “afar” “affray”
“afro” “arbor” “array”
“arroyo” “barb” “barf”
“boar” “boor” “bray”
“farro” “fora” “foray”
“fray” “parry” “poor”
“pray” “proof” “prop”
“pyro” “roar” “roof”
“ropy”})

(defn get-unique-letter-collection [word-set]
(-> word-set
vec
str/join
seq
set))

(defn find-common-letter [word-set]
(reduce
set/intersection
(map set (seq word-set))))

(defn word-validity-case [word letters common-letter]
(cond
(contains? word-collection word) :submit-ok ; first check if the word is in the word-collection
(> 4 (count (seq word))) :too-short ; check length, notify if less than 3 letters
(not (every? letters (set word))) :invalid ; check if every letter in the word is in letters set
(not (contains? (set word) (first common-letter))) :no-common ; if it does not contain the common letter
(contains? (set word) (first common-letter)) :not-in-list ; then check if the word at least contains common letter
:else :other)) ; generic if it somehow manages to not match one of the above

(defn point-formula [word letters]
(cond
(= (get-unique-letter-collection word) (set letters)) (+ (count (seq word)) 7)
(= (count (seq word)) 4) (int 1)
:else (count (seq word))))

;---------- subscriptions to data from app state ----------

(rf/reg-sub
::name
(fn [db]
(:name db)))

(rf/reg-sub
::game-started
(fn [db]
(:game-started db)))

(rf/reg-sub
::words
(fn [db]
(:words db)))

(rf/reg-sub
::found-words
(fn [db]
(:found-words db)))

(rf/reg-sub
::common-letter
(fn [db]
(:common-letter db)))

(rf/reg-sub
::letters
(fn [db]
(:letters db)))

(rf/reg-sub
::display-letters
(fn [db]
(:display-letters db)))

(rf/reg-sub
::current-input
(fn [db]
(:current-input db)))

(rf/reg-sub
::message
(fn [db]
(:message db)))

(rf/reg-sub
::score
(fn [db]
(:score db)))

(rf/reg-sub
::dbdb
(fn [db]
db))



;---------- events ----------

(rf/reg-event-db
::initialize-db
(fn [_ ]
default-db))

(rf/reg-event-db ::set-words-and-letters
(fn [db [ word-set]]
(let [common-letter (find-common-letter word-set)
letter-coll (get-unique-letter-collection word-set)]
(assoc db :words word-set
:common-letter common-letter
:letters letter-coll
:display-letters (shuffle (vec (remove common-letter letter-coll)))
:game-started true))))

(rf/reg-event-db ::update-current-input
(fn [db [_ input-value]]
(assoc db :current-input input-value)))

(rf/reg-event-db ::shuffle-letter-order
(fn [db [_ display-letters]]
(assoc db :display-letters (shuffle display-letters))))

(rf/reg-event-db ::submit-word
(fn [db [_ word]]
(let [letters (:letters db)
common-letter (:common-letter db)
point-val (point-formula word letters)]
(case (word-validity-case word letters common-letter)
:submit-ok (if (contains? (:found-words db) word)
(assoc db :message “You’ve already found that word!”)
(-> db
(update :found-words conj word)
(update :score + point-val)
(assoc :message (str "Great job! You found " word ", worth a score of " point-val “!”)))) ; add the valid word to found words
:too-short (assoc db :message “Only words with 4 letters or more count.”)
:not-in-list (assoc db :message (str “Sorry, " word " isn’t in the word list today.”))
:no-common (assoc db :message “Nice try, but the word needs to contain the common letter.”)
:invalid (assoc db :message “All letters in the word must be from the given letter set.”)
:other (assoc db :message “Try again.”)))))



;---------- stylefy components ----------

(def main-style
{:padding “20px”
:max-width “600px”
:margin “0 auto”
:font-family “‘Open Sans’, sans-serif”
:border “3px solid #ccc”})

(def button-style
{:background-color “#4CAF50”
:border “none”
:color “white”
:padding “15px 32px”
:text-align “center”
:text-decoration “none”
:display “inline-block”
:font-size “16px”
:margin “4px 2px”
:cursor “pointer”})

(def input-style
{:padding “10px”
:border “4px solid #fcc”
:border-radius “3px”
:margin-bottom “5px”
:font-size “18px”
:font-family “inherit”})

(def side-panel-style
{:flex “1”
:padding “10px”
:margin-left “20px”
:border “3px solid #ccc”
:border-radius “4px”
:background-color “#F0FFFF”
:max-width “200px”})

(def main-panel-style
{:max-width “280px”})

(def main-container-style
{:display “flex”
:justify-content “space-around”
})

(def body-background
{:background-image “url(‘/images/tokimemobgscroll.gif’)”
:background-size “cover”
:background-position “center center”
:background-repeat “no-repeat”})


;---------- main page elements ----------

(defn spawn-words-button
“Starts the game with a preset set of words.”
[]
(let [game-started (rf/subscribe [::game-started])]
(when-not @game-started
[:button
{:on-click #(rf/dispatch [::set-words-and-letters word-collection])
:style button-style}
“Get Letters!”])))

(defn submit-button
[word]
(let [input-value (rf/subscribe [::current-input])]
[:button
{:on-click #(when (seq word)
(println “click!”)
(rf/dispatch [::submit-word @input-value])
(rf/dispatch [::update-current-input “”])) ; clear input after submit
:style button-style}
“Submit”]))

(defn text-input
“Field for the user to input a word of their choosing.”
[]
(let [input-value (rf/subscribe [::current-input])]
[:input {:type “text”
:placeholder “Type here!”
:value @input-value
:on-change #(rf/dispatch [::update-current-input (-> % .-target .-value)])
:style input-style}]))

(defn shuffle-order-button
“Shuffles the order of the letters displayed.”
[display-letters]
[:button {:on-click #(rf/dispatch [::shuffle-letter-order display-letters])
:style button-style}
“Shuffle letters”])



;---------- main page renderer ----------

(defn main-panel []
(let [name (rf/subscribe [::name])
game-started (rf/subscribe [::game-started])
words (rf/subscribe [::words])
found-words (rf/subscribe [::found-words])
common-letter (rf/subscribe [::common-letter])
letters (rf/subscribe [::letters])
display-letters (rf/subscribe [::display-letters])
current-input (rf/subscribe [::current-input])
message (rf/subscribe [::message])
score (rf/subscribe [::score])
database (rf/subscribe [::dbdb])]

[:html
[:head
[:title “Spelling Bee!”]

[:style {:id “stylefy-server-styles”} “stylefy-server-styles-content”]
[:style {:id “stylefy-constant-styles”}]
[:style {:id “stylefy-styles”}]]

[:div (use-style body-background)
[:div (use-style main-style)
[:h1
"Hello, " @name]
[:h3 @message]
[spawn-words-button]
(when @game-started
[:div (use-style main-container-style)
[:div (use-style main-panel-style)
[:div (use-style {:text-align “center”})
[text-input]
[submit-button @current-input]]

[:p "Common Letter: " (str (first @common-letter))]
[:p "Other Letters: " (str/join ", " @display-letters)]
[:div (use-style {:text-align “center”})
[shuffle-order-button @display-letters]]
[:h3 "Your score: " @score]]

[:div (use-style side-panel-style)
[:h3
“Found words:”]
[:ul (for [word (sort @found-words)] ; sort found words into an alphabetical list
[:li word])]]])
]]]))


;---------- page load parameters ----------

(defn dev-setup []
(when debug?
(println “dev mode”)))

(defn ^:dev/after-load mount-root []
(rf/clear-subscription-cache!)
(let [root-el (.getElementById js/document “app”)]
(rdom/unmount-component-at-node root-el)
(rdom/render [main-panel] root-el)))

(defn init []
(rf/dispatch-sync [::initialize-db])
(stylefy/init {:dom (stylefy-reagent/init)})
(dev-setup)
(mount-root))


How do I implement a global key listener?