|
1 | 1 | (ns rocks.mygiftlist.client |
2 | | - (:require [rocks.mygiftlist.application :refer [SPA]] |
3 | | - [rocks.mygiftlist.authentication :as auth] |
4 | | - [com.fulcrologic.fulcro.application :as app] |
5 | | - [com.fulcrologic.fulcro.components :as comp :refer [defsc]] |
6 | | - [com.fulcrologic.fulcro.dom :as dom] |
7 | | - [com.fulcrologic.fulcro.mutations :refer [defmutation]] |
8 | | - [clojure.core.async :refer [go <!]] |
9 | | - [clojure.string :as str] |
10 | | - [taoensso.timbre :as log])) |
11 | | - |
12 | | -(defsc CurrentUser [this {:user/keys [id email] |
13 | | - :ui/keys [loading] :as user}] |
14 | | - {:query [:user/id :user/email :ui/loading] |
15 | | - :ident (fn [] [:component/id :current-user]) |
16 | | - :initial-state {:ui/loading true}} |
17 | | - (cond |
18 | | - loading (dom/button :.ui.loading.primary.button) |
19 | | - (and id email) (dom/button :.ui.primary.button |
20 | | - {:onClick #(auth/logout)} |
21 | | - "Logout") |
22 | | - :else (dom/button :.ui.primary.button |
23 | | - {:onClick #(auth/login)} |
24 | | - "Login/Signup"))) |
25 | | - |
26 | | -(def ui-current-user (comp/factory CurrentUser)) |
27 | | - |
28 | | -(defsc Root [this {:root/keys [current-user]}] |
29 | | - {:query [{:root/current-user (comp/get-query CurrentUser)}] |
30 | | - :initial-state {:root/current-user {}}} |
| 2 | + (:require |
| 3 | + [rocks.mygiftlist.application :refer [SPA]] |
| 4 | + [rocks.mygiftlist.authentication :as auth] |
| 5 | + [com.fulcrologic.fulcro.algorithms.normalized-state :refer [swap!->]] |
| 6 | + [com.fulcrologic.fulcro.application :as app] |
| 7 | + [com.fulcrologic.fulcro.components :as comp :refer [defsc]] |
| 8 | + [com.fulcrologic.fulcro.dom :as dom] |
| 9 | + [com.fulcrologic.fulcro.mutations :refer [defmutation]] |
| 10 | + [com.fulcrologic.fulcro.routing.dynamic-routing :as dr :refer [defrouter]] |
| 11 | + [clojure.core.async :as async :refer [go <!]] |
| 12 | + [clojure.string :as str] |
| 13 | + [edn-query-language.core :as eql] |
| 14 | + [pushy.core :as pushy] |
| 15 | + [taoensso.timbre :as log])) |
| 16 | + |
| 17 | +(defn url->path |
| 18 | + "Given a url of the form \"/gift/123/edit?code=abcdef\", returns a |
| 19 | + path vector of the form [\"gift\" \"123\" \"edit\"]. Assumes the url |
| 20 | + starts with a forward slash. An empty url yields the path [\"home\"] |
| 21 | + instead of []." |
| 22 | + [url] |
| 23 | + (-> url (str/split "?") first (str/split "/") rest vec)) |
| 24 | + |
| 25 | +(defn path->url |
| 26 | + "Given a path vector of the form [\"gift\" \"123\" \"edit\"], |
| 27 | + returns a url of the form \"/gift/123/edit\"." |
| 28 | + [path] |
| 29 | + (str/join (interleave (repeat "/") path))) |
| 30 | + |
| 31 | +(defn routable-path? |
| 32 | + "True if there exists a router target for the given path." |
| 33 | + [app path] |
| 34 | + (let [state-map (app/current-state app) |
| 35 | + root-class (app/root-class app) |
| 36 | + root-query (comp/get-query root-class state-map) |
| 37 | + ast (eql/query->ast root-query)] |
| 38 | + (some? (dr/ast-node-for-route ast path)))) |
| 39 | + |
| 40 | +(def default-route ["home"]) |
| 41 | + |
| 42 | +(defonce history (pushy/pushy |
| 43 | + (fn [path] |
| 44 | + (dr/change-route SPA path)) |
| 45 | + (fn [url] |
| 46 | + (let [path (url->path url)] |
| 47 | + (if (routable-path? SPA path) |
| 48 | + path |
| 49 | + default-route))))) |
| 50 | + |
| 51 | +(defn start! [] |
| 52 | + (pushy/start! history)) |
| 53 | + |
| 54 | +(defn route-to! [path] |
| 55 | + (pushy/set-token! history (path->url path))) |
| 56 | + |
| 57 | +(defmutation route-to |
| 58 | + "Mutation to go to a specific route" |
| 59 | + [{:keys [path]}] |
| 60 | + (action [_] |
| 61 | + (route-to! path))) |
| 62 | + |
| 63 | +(defsc LoginLogoutItem [this {:ui/keys [authenticated]}] |
| 64 | + {:query [:ui/authenticated] |
| 65 | + :ident (fn [] [:component/id :login-logout]) |
| 66 | + :initial-state {:ui/authenticated nil}} |
| 67 | + (when (some? authenticated) |
| 68 | + (if authenticated |
| 69 | + (dom/a :.item {:onClick #(auth/logout)} "Logout") |
| 70 | + (dom/a :.item {:onClick #(auth/login)} "Login/Signup")))) |
| 71 | + |
| 72 | +(def ui-login-logout-item (comp/factory LoginLogoutItem)) |
| 73 | + |
| 74 | +(defmutation set-authenticated [{:keys [authenticated]}] |
| 75 | + (action [{:keys [state]}] |
| 76 | + (swap!-> state |
| 77 | + (assoc-in [:component/id :login-logout :ui/authenticated] authenticated) |
| 78 | + (assoc :root/loading false)))) |
| 79 | + |
| 80 | +(declare LoginForm Home About) |
| 81 | + |
| 82 | +(defsc Navbar [this {:keys [login-logout]}] |
| 83 | + {:query [{:login-logout (comp/get-query LoginLogoutItem)}] |
| 84 | + :ident (fn [] [:component/id :navbar]) |
| 85 | + :initial-state {:login-logout {}}} |
| 86 | + (let [logged-in (:ui/authenticated login-logout)] |
| 87 | + (dom/div :.ui.secondary.menu |
| 88 | + (dom/a :.item |
| 89 | + {:onClick #(comp/transact! this |
| 90 | + [(route-to {:path (dr/path-to (if logged-in |
| 91 | + Home |
| 92 | + LoginForm))})])} |
| 93 | + "Home") |
| 94 | + (dom/a :.item |
| 95 | + {:onClick #(comp/transact! this |
| 96 | + [(route-to {:path (dr/path-to About)})])} |
| 97 | + "About") |
| 98 | + (dom/div :.right.menu |
| 99 | + (ui-login-logout-item login-logout))))) |
| 100 | + |
| 101 | +(def ui-navbar (comp/factory Navbar)) |
| 102 | + |
| 103 | +(defsc LoginForm [this _] |
| 104 | + {:query [] |
| 105 | + :ident (fn [] [:component/id :login]) |
| 106 | + :route-segment ["login"] |
| 107 | + :initial-state {}} |
| 108 | + (dom/div {} |
| 109 | + (dom/div "In order to view and create gift lists, you need to...") |
| 110 | + (dom/div (dom/button :.ui.primary.button |
| 111 | + {:onClick #(auth/login)} |
| 112 | + "Log in or sign up")))) |
| 113 | + |
| 114 | +(defsc Home [this _] |
| 115 | + {:query [] |
| 116 | + :ident (fn [] [:component/id :home]) |
| 117 | + :initial-state {} |
| 118 | + :route-segment ["home"]} |
31 | 119 | (dom/div {} |
32 | | - (dom/div {} "Hello World") |
33 | | - (ui-current-user current-user))) |
| 120 | + (dom/h3 {} "Home Screen") |
| 121 | + (dom/div {} "Welcome!") |
| 122 | + (dom/button :.ui.primary.button |
| 123 | + {:onClick #(auth/logout)} |
| 124 | + "Logout"))) |
| 125 | + |
| 126 | +(defsc About [this _] |
| 127 | + {:query [] |
| 128 | + :ident (fn [] [:component/id :home]) |
| 129 | + :initial-state {} |
| 130 | + :route-segment ["about"]} |
| 131 | + (dom/div {} |
| 132 | + (dom/h3 {} "About My Gift List") |
| 133 | + (dom/div {} "It's a really cool app!"))) |
| 134 | + |
| 135 | +(defn loading-spinner [] |
| 136 | + (dom/div :.ui.active.inverted.dimmer |
| 137 | + (dom/div :.ui.loader))) |
| 138 | + |
| 139 | +(defsc Loading [this _] |
| 140 | + {:query [] |
| 141 | + :ident (fn [] [:component/id ::loading]) |
| 142 | + :initial-state {} |
| 143 | + :route-segment ["loading"]} |
| 144 | + (loading-spinner)) |
| 145 | + |
| 146 | +(defrouter MainRouter [_ {:keys [current-state] :as props}] |
| 147 | + {:router-targets [Loading LoginForm Home About]} |
| 148 | + (loading-spinner)) |
| 149 | + |
| 150 | +(def ui-main-router (comp/factory MainRouter)) |
| 151 | + |
| 152 | +(defsc Root [this {:root/keys [router navbar loading]}] |
| 153 | + {:query [{:root/router (comp/get-query MainRouter)} |
| 154 | + {:root/navbar (comp/get-query Navbar)} |
| 155 | + :root/loading] |
| 156 | + :initial-state {:root/router {} |
| 157 | + :root/navbar {} |
| 158 | + :root/loading true}} |
| 159 | + (if loading |
| 160 | + (loading-spinner) |
| 161 | + (dom/div {} |
| 162 | + (ui-navbar navbar) |
| 163 | + (dom/div :.ui.container |
| 164 | + (ui-main-router router))))) |
34 | 165 |
|
35 | 166 | (defmutation set-current-user [user] |
36 | 167 | (action [{:keys [state]}] |
|
40 | 171 | (log/info "Hot code reload...") |
41 | 172 | (app/mount! SPA Root "app")) |
42 | 173 |
|
| 174 | +(defn- is-redirect? [] |
| 175 | + (str/includes? (.. js/window -location -search) "code=")) |
| 176 | + |
| 177 | +(defn- clear-query-params! [] |
| 178 | + (.replaceState js/window.history #js {} js/document.title js/window.location.pathname)) |
| 179 | + |
43 | 180 | (defn ^:export init [] |
44 | 181 | (log/info "Application starting...") |
45 | 182 | (app/mount! SPA Root "app") |
| 183 | + (dr/initialize! SPA) |
| 184 | + (pushy/start! history) |
46 | 185 | (go |
47 | 186 | (<! (auth/create-auth0-client!)) |
48 | | - (when (str/includes? (.. js/window -location -search) "code=") |
| 187 | + (when (is-redirect?) |
49 | 188 | (<! (auth/handle-redirect-callback)) |
50 | | - (.replaceState js/window.history #js {} js/document.title js/window.location.pathname)) |
51 | | - (if-let [authenticated (<! (auth/is-authenticated?))] |
52 | | - (let [{:strs [sub email]} (js->clj (<! (auth/get-user-info)))] |
53 | | - (comp/transact! SPA [(set-current-user {:user/id sub :user/email email})])) |
54 | | - (comp/transact! SPA [(set-current-user {})])))) |
| 189 | + (clear-query-params!)) |
| 190 | + (let [authenticated (<! (auth/is-authenticated?))] |
| 191 | + (comp/transact! SPA [(set-authenticated |
| 192 | + {:authenticated authenticated})]) |
| 193 | + (if authenticated |
| 194 | + (do (comp/transact! SPA |
| 195 | + [(route-to {:path (url->path js/window.location.pathname)})]) |
| 196 | + (let [{:keys [sub email]} (<! (auth/get-user-info))] |
| 197 | + (comp/transact! SPA [(set-current-user |
| 198 | + #:user{:id sub :email email})]))) |
| 199 | + (comp/transact! SPA |
| 200 | + [(route-to {:path (dr/path-to LoginForm)})]))))) |
0 commit comments