Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Implement HTML5 routing.
  • Loading branch information
codonnell committed Mar 27, 2020
1 parent 2ca04e7 commit 5b7990a
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 46 deletions.
2 changes: 2 additions & 0 deletions .dir-locals.el
@@ -0,0 +1,2 @@
((nil . ((fill-column . 70)
(column-enforce-column . 70))))
14 changes: 8 additions & 6 deletions deps.edn
Expand Up @@ -3,9 +3,11 @@
com.taoensso/timbre {:mvn/version "4.10.0"}}
:aliases {:dev {:extra-paths ["dev"]
:jvm-opts ["-Dtrace"]
:extra-deps {org.clojure/clojurescript {:mvn/version "1.10.597"}
org.clojure/core.async {:mvn/version "1.0.567"}
com.wsscode/async {:mvn/version "1.0.2"}
thheller/shadow-cljs {:mvn/version "2.8.83"}
fulcrologic/fulcro-inspect {:mvn/version "2.2.5"}
binaryage/devtools {:mvn/version "0.9.10"}}}}}
:extra-deps {org.clojure/clojurescript {:mvn/version "1.10.597"}
com.fulcrologic/semantic-ui-wrapper {:mvn/version "1.0.0"}
org.clojure/core.async {:mvn/version "1.0.567"}
com.wsscode/async {:mvn/version "1.0.2"}
clj-commons/pushy {:mvn/version "0.3.10"}
edn-query-language/eql {:mvn/version "0.0.9"}
thheller/shadow-cljs {:mvn/version "2.8.83"}
binaryage/devtools {:mvn/version "0.9.10"}}}}}
5 changes: 3 additions & 2 deletions src/rocks/mygiftlist/application.cljs
@@ -1,4 +1,5 @@
(ns rocks.mygiftlist.application
(:require [com.fulcrologic.fulcro.application :as app]))
(:require [com.fulcrologic.fulcro.application :as app]
[com.fulcrologic.fulcro.rendering.keyframe-render2 :as keyframe-render2]))

(defonce SPA (app/fulcro-app {}))
(defonce SPA (app/fulcro-app {:optimized-render! keyframe-render2/render!}))
3 changes: 2 additions & 1 deletion src/rocks/mygiftlist/authentication.cljs
Expand Up @@ -32,4 +32,5 @@
(go-promise (<!p (.getTokenSilently @auth0-client))))

(defn get-user-info []
(go-promise (<!p (.getUser @auth0-client))))
(go-promise (js->clj (<!p (.getUser @auth0-client))
:keywordize-keys true)))
220 changes: 183 additions & 37 deletions src/rocks/mygiftlist/client.cljs
@@ -1,36 +1,167 @@
(ns rocks.mygiftlist.client
(:require [rocks.mygiftlist.application :refer [SPA]]
[rocks.mygiftlist.authentication :as auth]
[com.fulcrologic.fulcro.application :as app]
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.dom :as dom]
[com.fulcrologic.fulcro.mutations :refer [defmutation]]
[clojure.core.async :refer [go <!]]
[clojure.string :as str]
[taoensso.timbre :as log]))

(defsc CurrentUser [this {:user/keys [id email]
:ui/keys [loading] :as user}]
{:query [:user/id :user/email :ui/loading]
:ident (fn [] [:component/id :current-user])
:initial-state {:ui/loading true}}
(cond
loading (dom/button :.ui.loading.primary.button)
(and id email) (dom/button :.ui.primary.button
{:onClick #(auth/logout)}
"Logout")
:else (dom/button :.ui.primary.button
{:onClick #(auth/login)}
"Login/Signup")))

(def ui-current-user (comp/factory CurrentUser))

(defsc Root [this {:root/keys [current-user]}]
{:query [{:root/current-user (comp/get-query CurrentUser)}]
:initial-state {:root/current-user {}}}
(:require
[rocks.mygiftlist.application :refer [SPA]]
[rocks.mygiftlist.authentication :as auth]
[com.fulcrologic.fulcro.algorithms.normalized-state :refer [swap!->]]
[com.fulcrologic.fulcro.application :as app]
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.dom :as dom]
[com.fulcrologic.fulcro.mutations :refer [defmutation]]
[com.fulcrologic.fulcro.routing.dynamic-routing :as dr :refer [defrouter]]
[clojure.core.async :as async :refer [go <!]]
[clojure.string :as str]
[edn-query-language.core :as eql]
[pushy.core :as pushy]
[taoensso.timbre :as log]))

(defn url->path
"Given a url of the form \"/gift/123/edit?code=abcdef\", returns a
path vector of the form [\"gift\" \"123\" \"edit\"]. Assumes the url
starts with a forward slash. An empty url yields the path [\"home\"]
instead of []."
[url]
(-> url (str/split "?") first (str/split "/") rest vec))

(defn path->url
"Given a path vector of the form [\"gift\" \"123\" \"edit\"],
returns a url of the form \"/gift/123/edit\"."
[path]
(str/join (interleave (repeat "/") path)))

(defn routable-path?
"True if there exists a router target for the given path."
[app path]
(let [state-map (app/current-state app)
root-class (app/root-class app)
root-query (comp/get-query root-class state-map)
ast (eql/query->ast root-query)]
(some? (dr/ast-node-for-route ast path))))

(def default-route ["home"])

(defonce history (pushy/pushy
(fn [path]
(dr/change-route SPA path))
(fn [url]
(let [path (url->path url)]
(if (routable-path? SPA path)
path
default-route)))))

(defn start! []
(pushy/start! history))

(defn route-to! [path]
(pushy/set-token! history (path->url path)))

(defmutation route-to
"Mutation to go to a specific route"
[{:keys [path]}]
(action [_]
(route-to! path)))

(defsc LoginLogoutItem [this {:ui/keys [authenticated]}]
{:query [:ui/authenticated]
:ident (fn [] [:component/id :login-logout])
:initial-state {:ui/authenticated nil}}
(when (some? authenticated)
(if authenticated
(dom/a :.item {:onClick #(auth/logout)} "Logout")
(dom/a :.item {:onClick #(auth/login)} "Login/Signup"))))

(def ui-login-logout-item (comp/factory LoginLogoutItem))

(defmutation set-authenticated [{:keys [authenticated]}]
(action [{:keys [state]}]
(swap!-> state
(assoc-in [:component/id :login-logout :ui/authenticated] authenticated)
(assoc :root/loading false))))

(declare LoginForm Home About)

(defsc Navbar [this {:keys [login-logout]}]
{:query [{:login-logout (comp/get-query LoginLogoutItem)}]
:ident (fn [] [:component/id :navbar])
:initial-state {:login-logout {}}}
(let [logged-in (:ui/authenticated login-logout)]
(dom/div :.ui.secondary.menu
(dom/a :.item
{:onClick #(comp/transact! this
[(route-to {:path (dr/path-to (if logged-in
Home
LoginForm))})])}
"Home")
(dom/a :.item
{:onClick #(comp/transact! this
[(route-to {:path (dr/path-to About)})])}
"About")
(dom/div :.right.menu
(ui-login-logout-item login-logout)))))

(def ui-navbar (comp/factory Navbar))

(defsc LoginForm [this _]
{:query []
:ident (fn [] [:component/id :login])
:route-segment ["login"]
:initial-state {}}
(dom/div {}
(dom/div "In order to view and create gift lists, you need to...")
(dom/div (dom/button :.ui.primary.button
{:onClick #(auth/login)}
"Log in or sign up"))))

(defsc Home [this _]
{:query []
:ident (fn [] [:component/id :home])
:initial-state {}
:route-segment ["home"]}
(dom/div {}
(dom/div {} "Hello World")
(ui-current-user current-user)))
(dom/h3 {} "Home Screen")
(dom/div {} "Welcome!")
(dom/button :.ui.primary.button
{:onClick #(auth/logout)}
"Logout")))

(defsc About [this _]
{:query []
:ident (fn [] [:component/id :home])
:initial-state {}
:route-segment ["about"]}
(dom/div {}
(dom/h3 {} "About My Gift List")
(dom/div {} "It's a really cool app!")))

(defn loading-spinner []
(dom/div :.ui.active.inverted.dimmer
(dom/div :.ui.loader)))

(defsc Loading [this _]
{:query []
:ident (fn [] [:component/id ::loading])
:initial-state {}
:route-segment ["loading"]}
(loading-spinner))

(defrouter MainRouter [_ {:keys [current-state] :as props}]
{:router-targets [Loading LoginForm Home About]}
(loading-spinner))

(def ui-main-router (comp/factory MainRouter))

(defsc Root [this {:root/keys [router navbar loading]}]
{:query [{:root/router (comp/get-query MainRouter)}
{:root/navbar (comp/get-query Navbar)}
:root/loading]
:initial-state {:root/router {}
:root/navbar {}
:root/loading true}}
(if loading
(loading-spinner)
(dom/div {}
(ui-navbar navbar)
(dom/div :.ui.container
(ui-main-router router)))))

(defmutation set-current-user [user]
(action [{:keys [state]}]
Expand All @@ -40,15 +171,30 @@
(log/info "Hot code reload...")
(app/mount! SPA Root "app"))

(defn- is-redirect? []
(str/includes? (.. js/window -location -search) "code="))

(defn- clear-query-params! []
(.replaceState js/window.history #js {} js/document.title js/window.location.pathname))

(defn ^:export init []
(log/info "Application starting...")
(app/mount! SPA Root "app")
(dr/initialize! SPA)
(pushy/start! history)
(go
(<! (auth/create-auth0-client!))
(when (str/includes? (.. js/window -location -search) "code=")
(when (is-redirect?)
(<! (auth/handle-redirect-callback))
(.replaceState js/window.history #js {} js/document.title js/window.location.pathname))
(if-let [authenticated (<! (auth/is-authenticated?))]
(let [{:strs [sub email]} (js->clj (<! (auth/get-user-info)))]
(comp/transact! SPA [(set-current-user {:user/id sub :user/email email})]))
(comp/transact! SPA [(set-current-user {})]))))
(clear-query-params!))
(let [authenticated (<! (auth/is-authenticated?))]
(comp/transact! SPA [(set-authenticated
{:authenticated authenticated})])
(if authenticated
(do (comp/transact! SPA
[(route-to {:path (url->path js/window.location.pathname)})])
(let [{:keys [sub email]} (<! (auth/get-user-info))]
(comp/transact! SPA [(set-current-user
#:user{:id sub :email email})])))
(comp/transact! SPA
[(route-to {:path (dr/path-to LoginForm)})])))))

0 comments on commit 5b7990a

Please sign in to comment.