I’m making a spelling bee game with ClojureScript. Here is my code.

(ns spelling-bee.core
  (:require 
   [clojure.string      :as str]
   [re-frame.core       :as rf]
   [reagent.core        :as ra]
   [reagent.dom         :as rdom]
   [stylefy.core        :as stylefy :refer [use-style]]
   [stylefy.reagent     :as stylefy-reagent]
   [spelling-bee.events :as events]
   [spelling-bee.words  :as words])
  (:require-macros
   [reagent.core :refer [with-let]]))

(def debug?
  ^boolean goog.DEBUG)

(set! *warn-on-infer* false)


;---------- stylefy components ----------
; px vs rem, google
(defn letter-style [letter-validation-sequence]
  (case letter-validation-sequence
    :required {:color "#4CAF50"}
    :valid    {:color "#000000"}
    :invalid  {:color "#AAAAAA" :opacity "0.5"}))



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

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

(defn submit-button 
  [word]
  (let [input-value (rf/subscribe [::events/current-input])]
    [:button
     {:on-click #(when (seq word)
                   (println "click!")
                   (rf/dispatch [::events/submit-word @input-value])) 
      :class "button-style"} 
     "Submit"]))

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

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



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

(defn main-panel []
  #_{:clj-kondo/ignore [:unresolved-symbol]}
  (with-let [
             name            (rf/subscribe [::events/name])
             game-started    (rf/subscribe [::events/game-started])
             words           (rf/subscribe [::events/words])
             found-words     (rf/subscribe [::events/found-words])
             common-letter   (rf/subscribe [::events/common-letter])
             letters         (rf/subscribe [::events/letters])
             display-letters (rf/subscribe [::events/display-letters])
             current-input   (rf/subscribe [::events/current-input])
             message         (rf/subscribe [::events/message])
             score           (rf/subscribe [::events/score])
             database        (rf/subscribe [::events/dbdb])
             shake-message?  (rf/subscribe [::events/shake-message?])]
    
    
     
      [:div
       [:div {:class "main-style"}
        [:h1
         "Hello, " @name]
       [:p "debug: "@database]
        [:h3 {:class (str ""(when @shake-message? "shake"))} @message]
        [spawn-words-button]
        (when @game-started 
          [:div {:class "main-container-style"}
           [:div {:class "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 {:class "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 install-global-key-listeners []
  (.addEventListener js/window "keydown" events/global-key-handler))

(defn init []
  (install-global-key-listeners)               ; listen for keypress events
  (rf/dispatch-sync [::events/initialize-db])         ; get re-frame atom initialized
  (stylefy/init {:dom (stylefy-reagent/init)}) ; set up css
  (dev-setup)
  (mount-root))


(ns spelling-bee.events
  (:require
   [clojure.set         :as set]
   [clojure.string      :as str]
   [re-frame.core     :as rf]))


;---------- 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
   :shake-message     false})

;---------- handlers ----------

(defn global-key-handler [e]
  (let [key         (.-key e)
        input-value (rf/subscribe [::current-input])]
    (cond
      (re-matches #"[a-zA-Z]" key)
      (rf/dispatch [::append-current-input (str key)])
      
      (= key "Enter")
      (rf/dispatch [::submit-word @input-value])

      (= key "Backspace")
      (let [subtract-letter #(subs % 0 (dec (count %)))]
        (rf/dispatch [::set-current-input (subtract-letter @input-value)]))

      :else
      nil)))
; remove subscribe, do in functions

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

;; Later this can be substituted with a database call to pull a list of words.


(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 validate-word
  "Checks the given word against the current word list and letter set to see if it is valid. Gives the following keywords as a result.
   
   :submit-ok :too-short :invalid :no-common :not-in-list :other"
  [word word-list letters common-letter]
  (cond
    (contains? word-list 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 validate-letter [letter letters common-letter]
  (cond
    (= letter (str (first common-letter))) :required
    (contains? (set letters) letter) :valid
    :else                            :invalid))

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

;; (map #(validate-letter #{}) (seq "arroyo"))

;---------- 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))

(rf/reg-sub ::shake-message?
  (fn [db]
    (:shake-message 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 ::set-current-input
  (fn [db [_ input-value]]
    (assoc db :current-input input-value)))

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

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

;; (rf/reg-fx
;;  ::trigger-shake
;;  (fn [] 
;;    (.classList.add root “shake”)
;;    (js/setTimeout
;;     #(when root
;;        (.classList.remove root “shake”))
;;     500)))

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


I want to make the message shake event happen every time the user inputs that incorrect value, I am assuming I'd need some sort of timer to set it back?