diff --git a/dev.cljs.edn b/dev.cljs.edn index 17155bf..9138c08 100644 --- a/dev.cljs.edn +++ b/dev.cljs.edn @@ -2,4 +2,4 @@ :optimizations :none :pretty-print true :source-map true - :main DEMOAPP.core} + :main weecfg.core} diff --git a/prod.cljs.edn b/prod.cljs.edn index b455031..564a899 100644 --- a/prod.cljs.edn +++ b/prod.cljs.edn @@ -1,6 +1,5 @@ {:output-to "app/data/main.js" - :output-dir "app/data/prod" - :asset-path "app/data/prod", + :output-dir "app/data/" + :asset-path "app/data/", :optimizations :advanced -; :source-map none - :main DEMOAPP.core} + :main weecfg.core} diff --git a/resources/public/form.edn b/resources/public/form.edn new file mode 100644 index 0000000..f39b8c0 --- /dev/null +++ b/resources/public/form.edn @@ -0,0 +1,12 @@ +[{:name "server" :title "Server" :placeholder "127.0.0.1" + :validator "server-type" + :error "Invalid server address"} + {:name "device-id" :title "Device Unique ID" :placeholder "AAAA" + :validator "device-id-type" + :error "Invalid identifier" :focus "true"} + {:name "wifi-ssid" :title "WIFI SSID" + :validator "wifi-ssid-type" + :error "Invalid SSID"} + {:name "wifi-key" :title "WIFI Password" + :validator "wifi-key-type" + :error "Invalid wifi password" :type "password"}] diff --git a/src/DEMOAPP/core.cljs b/src/weecfg/core.cljs similarity index 50% rename from src/DEMOAPP/core.cljs rename to src/weecfg/core.cljs index 08aa8f0..a4c4f0d 100644 --- a/src/DEMOAPP/core.cljs +++ b/src/weecfg/core.cljs @@ -1,63 +1,80 @@ -(ns DEMOAPP.core +(ns weecfg.core (:require [accountant.core :as accountant] [clojure.spec.alpha :as s] + [cognitect.transit :as t] + [clojure.edn :refer [read-string]] [ajax.core :refer [GET, POST]] - [reagent.core :as reagent :refer [atom]] [reagent.session :as session] [reitit.frontend :as reitit])) -;;(enable-console-print!) +; variables (defonce form-errors (atom nil)) (defonce form-title "GASP Sensor setup") (defonce form-data (atom {:server "" :device-id "" :wifi-ssid "" :wifi-key ""})) -(defonce form-layout [{:name "server" :title "Server" :placeholder "127.0.0.1" - :data form-data :validator "server-type" - :error "Invalid server address"} - {:name "device-id" :title "Device Unique ID" :placeholder "AAAA" - :data form-data :validator "device-id-type" - :error "Invalid identifier" :focus "true"} - {:name "wifi-ssid" :title "WIFI SSID" - :data form-data :validator "wifi-ssid-type" - :error "Invalid SSID"} - {:name "wifi-key" :title "WIFI Password" - :data form-data :validator "wifi-key-type" - :error "Invalid wifi password" :type "password"}]) - +(defonce form-layout (atom [])) (defonce device-id-regex #"[A-Za-z0-9]{4}") (defonce ssid-regex #"^[!#;].|[+\[\]\"\t\s].*$") -(s/def ::wifi-ssid-type (s/and string? #(re-matches ssid-regex %))) + +; spec validation definitions +(s/def ::size-test #(<= (count %) 2)) +(s/def ::ssid-size-limit #(<= (count %) 32)) +(s/def ::default-size-limit #(<= (count %) 50)) +(s/def ::wifi-ssid-type (s/and string? #(re-matches ssid-regex %) ::ssid-size-limit)) (s/def ::device-id-type (s/and string? #(re-matches device-id-regex %))) -(s/def ::wifi-key-type string?) +(s/def ::wifi-key-type (s/and string? ::default-size-limit)) +(s/def ::server-type (s/and string? ::default-size-limit)) + ; page routes + + (def router (reitit/router [["/" :index] - ["/404" :about]])) + ["/success" :success] + ["/about" :about] + ["/404" :error]])) -; link builder +; link builder give a keyword from the router above return a url (defn path-for [route & [params]] (if params (:path (reitit/match-by-name router route params)) (:path (reitit/match-by-name router route)))) -(defn form-input-map [{:keys [name title placeholder data validator error focus type]}] +(defn validation-message [errors default] + (let [pred (:cljs.spec.alpha/spec errors)] + (case pred + :cljs.core/string? "Sorry value must be a string!" + :weecfg.core/size-test "Sorry value to long!" + :weecfg.core/wifi-ssid-type "Sorry ssid invalid!" + :weecfg.core/wifi-key-type "Invalid wifi key" + :weecfg.core/device-id-type "Device is to short or invalid format." + (or (str default pred) (str "Error not handled " (str pred)))))) + + +; generate form fields dynamically from a hash map + + +(defn form-input-map [{:keys [name title placeholder validator error focus type]} data] [:div.mt3.dim {:key name} [:label.db.fw4.lh-copy.f6 {:for name} title] [:input#wifi-ssid.pa2.input-reset.ba.bg-near-white.w-100.measure.b--silver {:type (or type "text") :name name :placeholder placeholder :autoFocus (or focus nil) - :on-change #(swap! form-data assoc :device-id (-> % .-target .-value)) - :value (get @form-data :device-id) - }] + :on-change #(swap! data assoc (keyword name) (-> % .-target .-value)) + :value (get @data (keyword name))}] [:p (if (and (not-empty ((keyword name) @data)) (s/explain-data - (keyword 'DEMOAPP.core validator) + (keyword 'weecfg.core validator) ((keyword name) @data))) - error)]]) + (validation-message (s/explain-data + (keyword 'weecfg.core validator) + ((keyword name) @data)) error))]]) + + +; handle failures -(swap! form-data assoc :device-id "aa") (defn handle-failure [{:keys [status status-text response]}] (case status @@ -66,40 +83,49 @@ 404 (do (reset! form-errors "Could not find device to submit data.")) (reset! form-errors (str (:msg response) (:msg response))))) +; load in an edn map to build the form +(defn load-form [form-fields] + (GET "/form.edn" + {:response :transit + :format :transit + :error-handler handle-failure + :handler (fn [response] (reset! form-fields (read-string response)))})) + +;submit the data as raw post data (defn form-submit [data] (POST "/submit" {:params @data :error-handler handle-failure - ;:response-format :json :format :raw - ;:keywords? true :handler (fn [response] (swap! form-errors "success"))})) -; simple input form +;simple input form (defn config-form [] - {:on-submit - (fn [e] (.preventDefault e) - (api/save-snippet @title @text))} - [:article.black-80.db.tc.black.ba.w-100.bg-light-green.br2 - [:form.ma3 {:method "get" :accept-charset "utf-8" - :on-submit (fn [e] (.preventDefault e) (form-submit form-data))} - [:fieldset#sign_up.ba.b--transparent.ph0.mh0 - [:legend.ph0.pb3.mh0.fw6 "Device Setup"] - (if @form-errors - [:div.flex.items-center.justify-center.pa3.bg-lightest-blue.navy - [:i.fas.fa-exclamation-circle] - [:span.lh-title.ml3 @form-errors]]) - (map form-input-map form-layout)] - [:div.mt3 - [:input.b.ph3.pv2.input-reset.ba.b--black.bg-transparent.grow.pointer.f6.br2 - {:type "submit" :value "Save"}]]]]) + (let [form-fields (atom [])] + (load-form form-fields) + (fn [] + [:article.black-80.db.tc.black.ba.w-100.bg-light-green.br2 + [:form.ma3 {:method "get" :accept-charset "utf-8" + :on-submit (fn [e] (.preventDefault e) (form-submit form-data))} + (into [:fieldset#sign_up.ba.b--transparent.ph0.mh0 + [:legend.ph0.pb3.mh0.fw6 "Device Setup"]] + (map #(form-input-map % form-data) @form-fields)) + (if @form-errors + [:div.flex.items-center.justify-center.pa3.bg-lightest-blue.navy + [:i.fas.fa-exclamation-circle] + [:span.lh-title.ml3 @form-errors]]) + [:div.mt3 + [:input.b.ph3.pv2.input-reset.ba.b--black.bg-transparent.grow.pointer.f6.br2 + {:type "submit" :value "Save"}]]]]))) +; homepage is a simple form to enter your data (defn home-page [] - (config-form)) + [:div + [config-form]]) (defn success-page [] [:div @@ -110,17 +136,18 @@ [:div [:div [:h1.m-4 "About Page"] [:p "Sample app, demoing using ajax calls and reagent to render the page with some simple route from reitit, basically enought to get you started on your clojurescript journey."] + [:a.link {:href (path-for :index)} "Back"]]]) - [:a.navbar-item {:href (path-for :index)} "Home Page"]]]) - +; give a browser route load the relevant defn handler (defn page-for [route] "Map your route keys to pages here" (case route :index #'home-page + :error #'home-page + :success #'success-page :about #'about-page)) -; default page wrapper -; default page wrapper +; default page wrapper html (defn current-page [] (fn [] (let [page (:current-page (session/get :route))] @@ -128,10 +155,12 @@ [:div.w-25 [:a.athelas.f3.link.white-70.hover-white.no-underline.flex.items-center.pa3 {:href "/"} - [:h2 + [:h2.w-100 + [:a.link.fr.black {:href (path-for :about)} [:i.fas.fa-question-circle]] [:i.fas.fa-surprise.mr3] "GASP Sensor setup"]] [page]]]))) +; sort out routing sessions and basically kickstart everything (defn startup [] (accountant/configure-navigation! {:nav-handler @@ -141,11 +170,11 @@ route-params (:path-params match)] (session/put! :route {:current-page (page-for current-page) :route-params route-params}))) - :path-exists? (fn [path] (boolean (reitit/match-by-path router path)))}) (accountant/dispatch-current!) - (reagent/render [current-page] (.getElementById js/document "app"))) + (reagent/render [current-page] + (.getElementById js/document "app"))) (startup)