With the above page finished, it's time to move on. I am building a page that is supposed to use bootstrap's cards to show up to 20 photos per page as clickable items. It is supposed to retrieve whatever photos are in the database and display that many items as a paginated view.

Here is some more code I am working with.

(ns jimmystore.ajax
  (:require
    [ajax.core :as ajax]
    [luminus-transit.time :as time]
    [cognitect.transit :as transit]
    [re-frame.core :as rf]))

(defn local-uri? [{:keys [uri]}]
  (not (re-find #"^\w+?://" uri)))

(defn default-headers [request]
  (if (local-uri? request)
    (-> request
        (update :headers #(merge {"x-csrf-token" js/csrfToken} %)))
    request))

;; injects transit serialization config into request options

(defn as-transit [opts]
  (merge {:format          (ajax/transit-request-format
                             {:writer (transit/writer :json time/time-serialization-handlers)})
          :response-format (ajax/transit-response-format
                             {:reader (transit/reader :json time/time-deserialization-handlers)})}
         opts))

(defn load-interceptors! []
  (swap! ajax/default-interceptors
         conj
         (ajax/to-interceptor {:name "default headers"
                               :request default-headers})))


(ns jimmystore.events
  (:require
    [re-frame.core :as rf]
    [ajax.core :as ajax]
    [reitit.frontend.easy :as rfe]
    [reitit.frontend.controllers :as rfc]))

;;dispatchers

(rf/reg-event-db
  :common/navigate
  (fn [db [_ match]]
    (def foo match)
    (let [old-match (:common/route db)
          new-match (assoc match :controllers
                           (rfc/apply-controllers (:controllers old-match) match))]
      (assoc db :common/route new-match))))

(rf/reg-fx
  :common/navigate-fx!
  (fn [[k & [params query]]]
    (rfe/push-state k params query)))

(rf/reg-event-fx
  :common/navigate!
  (fn [_ [_ url-key params query]]
    {:common/navigate-fx! [url-key params query]}))

(rf/reg-event-db :set-docs
  (fn [db [_ docs]]
    (assoc db :docs docs)))

(rf/reg-event-fx
  :fetch-docs
  (fn [_ _]
    {:http-xhrio {:method          :get
                  :uri             "/docs"
                  :response-format (ajax/raw-response-format)
                  :on-success       [:set-docs]}}))

(rf/reg-event-fx :test-get-api
 (fn [_ _]
   {:http-xhrio {:method          :get
                 :uri             "/test-get"
                 :response-format (ajax/json-response-format {:keywords? true})
                 :on-success       [:set-docs]}}))

(rf/reg-event-fx :test-post-api
  (fn [_ _]
    {:http-xhrio {:method          :post
                  :uri             "/test-post"
                  :params          {:test-post {:data 1 :foo :bar 2 "ASDASD"}}
                  :format          (ajax/json-request-format)
                  :response-format (ajax/json-response-format {:keywords? true})
                  :on-success      [:no-op]}}))

(rf/reg-event-fx :test-upload-file
 (fn [{:keys [db]} [_ reference-id type]]
   (let [form-data           (js/FormData.)
         {:keys [file size]} (get db :upload)]
     (.append form-data "file" file)
     (.append form-data "reference-id" reference-id)
     (.append form-data "type" (name type)) ; Keywords are converted to strings
     {:db         (assoc-in db [:api-service :block-ui] true)
      :http-xhrio {:method          :post
                   :uri             "/test-upload"
                   :timeout         60000
                   :body            form-data
                   :response-format (ajax/json-response-format {:keywords? true})
                   :on-success      [:no-op]
                   :on-failure      [:no-op]}})))

(defn- get-file-size [file]
  (.-size file))

(defn- get-file-name [file]
  (.-name file))

(defn- get-file-type [file]
  (.-type file))

(rf/reg-event-db :set-file-to-upload
 (fn [db [_ file]]                ;; Local url (for previews etc.)
   (assoc db :upload {:object-url (js/window.webkitURL.createObjectURL file)
                      :file       file
                      :size       (get-file-size file)
                      :name       (get-file-name file)
                      :type       (get-file-type file)})))


(rf/reg-sub :upload
 (fn [db _]
   (-> db :upload)))

(rf/reg-event-db
  :common/set-error
  (fn [db [_ error]]
    (assoc db :common/error error)))

(rf/reg-event-fx
  :page/init-home
  (fn [_ _]
    {:dispatch [:fetch-docs]}))

(rf/reg-event-db
 :no-op
 (fn [db  _]
   db))

;;subscriptions

(rf/reg-sub
  :common/route
  (fn [db _]
    (-> db :common/route)))

(rf/reg-sub
  :common/page-id
  :<- [:common/route]
  (fn [route _]
    (-> route :data :name)))

(rf/reg-sub
  :common/page
  :<- [:common/route]
  (fn [route _]
    (-> route :data :view)))

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

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


(ns jimmystore.core
  (:require
   [day8.re-frame.http-fx]
   [reagent.dom :as rdom]
   [reagent.core :as r]
   [re-frame.core :as rf]
   [goog.events :as events]
   [goog.history.EventType :as HistoryEventType]
   [markdown.core :refer [md->html]]
   [jimmystore.ajax :as ajax]
   [jimmystore.events]
   [reitit.core :as reitit]
   [reitit.frontend.easy :as rfe]
   [jimmystore.page-handler :as page-handler])
  (:import goog.History))

(defn nav-link [uri title page]
  [:a.navbar-item
   {:href   uri
    :class (when (= page @(rf/subscribe [:common/page-id])) :is-active)}
   title])

(defn navbar-item [key name]
[:li {:class "nav-item"}
 [:a {:class        "nav-link"
      :aria-current :page
      :href         (rfe/href key)}
  name]])

(defn navbar []
  [:header {:class "bg-body-secondary"}
   [:nav {:class "navbar navbar-expand-sm container-sm"}
    [:div {:class "container-fluid"}
     [:a {:class "navbar-brand" :href "#"} "Jimmy Store :3"]
     [:button.navbar-toggler {:type :button
                              :data-bs-toggle :collapse
                              :data-bs-target "#navbarSupportedContent"
                              :aria-controls  "navbarSupportedContent"
                              :aria-expanded  false
                              :aria-label     "Toggle navigation"}
      [:span.navbar-toggler-icon]]
     [:div {:class "collapse navbar-collapse" :id "navbarSupportedContent"}
      [:ul {:class "navbar-nav 
                    me-auto 
                    mb-2 
                    mb-lg-0"}
       (navbar-item :home       "Home")
       (navbar-item :about      "About") 
       (navbar-item :photo      "Photo Print")
       (navbar-item :test-page  "Test Page")]]]]])

(defn page []
  (if-let [page @(rf/subscribe [:common/page])]
    [:div
     [navbar]
     [page]]
    #_(rfe/push-state :home)))

(defn navigate! [match _]
  (rf/dispatch [:common/navigate match]))

(def router (reitit/router page-handler/pages))

(defn start-router! []
  (rfe/start!
   router
   navigate!
   {}))

;; -------------------------
;; Initialize app
(defn ^:dev/after-load mount-components []
  (rf/clear-subscription-cache!)
  (rdom/render [#'page] (.getElementById js/document "app")))

(defn init! []
  (start-router!)
  (ajax/load-interceptors!)
  (mount-components))


And the start of the page I'm building.

(ns jimmystore.pages.photo-page 
  (:require
   [reagent.core :as r]
   [re-frame.core :as rf]))

(defn photos-element []
  )

(defn page []
  [:div.container
   [:h3 "Select a photo to print."]
   [photos-element]])

(def page-info {:page-id :photo
                :view   #'page})