From 4f1cd18da8bbf18919212653d3ca7b48ae2702b1 Mon Sep 17 00:00:00 2001 From: "Niluka Satharasinghe (folcon)" Date: Sat, 9 Jun 2018 17:30:32 +0100 Subject: [PATCH 1/5] Generated with: `lein new luminus guestbook-datomic +datomic +re-frame` --- guestbook-datomic/.gitignore | 14 ++ guestbook-datomic/Capstanfile | 28 ++++ guestbook-datomic/Dockerfile | 8 ++ guestbook-datomic/Procfile | 1 + guestbook-datomic/README.md | 21 +++ .../clj/guestbook_datomic/dev_middleware.clj | 10 ++ .../env/dev/clj/guestbook_datomic/env.clj | 14 ++ .../dev/clj/guestbook_datomic/figwheel.clj | 12 ++ guestbook-datomic/env/dev/clj/user.clj | 21 +++ .../env/dev/cljs/guestbook_datomic/app.cljs | 13 ++ .../env/dev/resources/config.edn | 1 + .../env/dev/resources/logback.xml | 35 +++++ .../env/prod/clj/guestbook_datomic/env.clj | 11 ++ .../env/prod/cljs/guestbook_datomic/app.cljs | 7 + .../env/prod/resources/config.edn | 2 + .../env/prod/resources/logback.xml | 24 ++++ .../env/test/resources/config.edn | 1 + .../env/test/resources/logback.xml | 35 +++++ guestbook-datomic/figwheel_server.log | 0 guestbook-datomic/project.clj | 134 ++++++++++++++++++ guestbook-datomic/resources/docs/docs.md | 82 +++++++++++ .../resources/public/css/screen.css | 69 +++++++++ .../resources/public/favicon.ico | Bin 0 -> 1150 bytes .../resources/public/img/warning_clojure.png | Bin 0 -> 21846 bytes .../resources/templates/error.html | 55 +++++++ .../resources/templates/home.html | 46 ++++++ .../src/clj/guestbook_datomic/config.clj | 12 ++ .../src/clj/guestbook_datomic/core.clj | 50 +++++++ .../src/clj/guestbook_datomic/db/core.clj | 48 +++++++ .../src/clj/guestbook_datomic/handler.clj | 26 ++++ .../src/clj/guestbook_datomic/layout.clj | 37 +++++ .../src/clj/guestbook_datomic/middleware.clj | 81 +++++++++++ .../src/clj/guestbook_datomic/routes/home.clj | 16 +++ .../cljc/guestbook_datomic/validation.cljc | 2 + .../src/cljs/guestbook_datomic/ajax.cljs | 19 +++ .../src/cljs/guestbook_datomic/core.cljs | 93 ++++++++++++ .../src/cljs/guestbook_datomic/events.cljs | 26 ++++ .../clj/guestbook_datomic/test/handler.clj | 21 +++ .../cljs/guestbook_datomic/core_test.cljs | 9 ++ .../cljs/guestbook_datomic/doo_runner.cljs | 6 + 40 files changed, 1090 insertions(+) create mode 100644 guestbook-datomic/.gitignore create mode 100644 guestbook-datomic/Capstanfile create mode 100644 guestbook-datomic/Dockerfile create mode 100644 guestbook-datomic/Procfile create mode 100644 guestbook-datomic/README.md create mode 100644 guestbook-datomic/env/dev/clj/guestbook_datomic/dev_middleware.clj create mode 100644 guestbook-datomic/env/dev/clj/guestbook_datomic/env.clj create mode 100644 guestbook-datomic/env/dev/clj/guestbook_datomic/figwheel.clj create mode 100644 guestbook-datomic/env/dev/clj/user.clj create mode 100644 guestbook-datomic/env/dev/cljs/guestbook_datomic/app.cljs create mode 100644 guestbook-datomic/env/dev/resources/config.edn create mode 100644 guestbook-datomic/env/dev/resources/logback.xml create mode 100644 guestbook-datomic/env/prod/clj/guestbook_datomic/env.clj create mode 100644 guestbook-datomic/env/prod/cljs/guestbook_datomic/app.cljs create mode 100644 guestbook-datomic/env/prod/resources/config.edn create mode 100644 guestbook-datomic/env/prod/resources/logback.xml create mode 100644 guestbook-datomic/env/test/resources/config.edn create mode 100644 guestbook-datomic/env/test/resources/logback.xml create mode 100644 guestbook-datomic/figwheel_server.log create mode 100644 guestbook-datomic/project.clj create mode 100644 guestbook-datomic/resources/docs/docs.md create mode 100644 guestbook-datomic/resources/public/css/screen.css create mode 100644 guestbook-datomic/resources/public/favicon.ico create mode 100644 guestbook-datomic/resources/public/img/warning_clojure.png create mode 100644 guestbook-datomic/resources/templates/error.html create mode 100644 guestbook-datomic/resources/templates/home.html create mode 100644 guestbook-datomic/src/clj/guestbook_datomic/config.clj create mode 100644 guestbook-datomic/src/clj/guestbook_datomic/core.clj create mode 100644 guestbook-datomic/src/clj/guestbook_datomic/db/core.clj create mode 100644 guestbook-datomic/src/clj/guestbook_datomic/handler.clj create mode 100644 guestbook-datomic/src/clj/guestbook_datomic/layout.clj create mode 100644 guestbook-datomic/src/clj/guestbook_datomic/middleware.clj create mode 100644 guestbook-datomic/src/clj/guestbook_datomic/routes/home.clj create mode 100644 guestbook-datomic/src/cljc/guestbook_datomic/validation.cljc create mode 100644 guestbook-datomic/src/cljs/guestbook_datomic/ajax.cljs create mode 100644 guestbook-datomic/src/cljs/guestbook_datomic/core.cljs create mode 100644 guestbook-datomic/src/cljs/guestbook_datomic/events.cljs create mode 100644 guestbook-datomic/test/clj/guestbook_datomic/test/handler.clj create mode 100644 guestbook-datomic/test/cljs/guestbook_datomic/core_test.cljs create mode 100644 guestbook-datomic/test/cljs/guestbook_datomic/doo_runner.cljs diff --git a/guestbook-datomic/.gitignore b/guestbook-datomic/.gitignore new file mode 100644 index 0000000..00f42c8 --- /dev/null +++ b/guestbook-datomic/.gitignore @@ -0,0 +1,14 @@ +/target +/lib +/classes +/checkouts +pom.xml +dev-config.edn +test-config.edn +*.jar +*.class +/.lein-* +profiles.clj +/.env +.nrepl-port +/log diff --git a/guestbook-datomic/Capstanfile b/guestbook-datomic/Capstanfile new file mode 100644 index 0000000..268bd41 --- /dev/null +++ b/guestbook-datomic/Capstanfile @@ -0,0 +1,28 @@ + +# +# Name of the base image. Capstan will download this automatically from +# Cloudius S3 repository. +# +#base: cloudius/osv +base: cloudius/osv-openjdk8 + +# +# The command line passed to OSv to start up the application. +# +cmdline: /java.so -jar /guestbook-datomic/app.jar + +# +# The command to use to build the application. +# You can use any build tool/command (make/rake/lein/boot) - this runs locally on your machine +# +# For Leiningen, you can use: +#build: lein uberjar +# For Boot, you can use: +#build: boot build + +# +# List of files that are included in the generated image. +# +files: + /guestbook-datomic/app.jar: ./target/uberjar/guestbook-datomic.jar + diff --git a/guestbook-datomic/Dockerfile b/guestbook-datomic/Dockerfile new file mode 100644 index 0000000..dc5b4fd --- /dev/null +++ b/guestbook-datomic/Dockerfile @@ -0,0 +1,8 @@ +FROM java:8-alpine +MAINTAINER Your Name + +ADD target/uberjar/guestbook-datomic.jar /guestbook-datomic/app.jar + +EXPOSE 3000 + +CMD ["java", "-jar", "/guestbook-datomic/app.jar"] diff --git a/guestbook-datomic/Procfile b/guestbook-datomic/Procfile new file mode 100644 index 0000000..1e5afe0 --- /dev/null +++ b/guestbook-datomic/Procfile @@ -0,0 +1 @@ +web: java -cp target/uberjar/guestbook-datomic.jar clojure.main -m guestbook-datomic.core diff --git a/guestbook-datomic/README.md b/guestbook-datomic/README.md new file mode 100644 index 0000000..de672ba --- /dev/null +++ b/guestbook-datomic/README.md @@ -0,0 +1,21 @@ +# guestbook-datomic + +generated using Luminus version "2.9.12.62" + +FIXME + +## Prerequisites + +You will need [Leiningen][1] 2.0 or above installed. + +[1]: https://github.com/technomancy/leiningen + +## Running + +To start a web server for the application, run: + + lein run + +## License + +Copyright © 2018 FIXME diff --git a/guestbook-datomic/env/dev/clj/guestbook_datomic/dev_middleware.clj b/guestbook-datomic/env/dev/clj/guestbook_datomic/dev_middleware.clj new file mode 100644 index 0000000..037ed8e --- /dev/null +++ b/guestbook-datomic/env/dev/clj/guestbook_datomic/dev_middleware.clj @@ -0,0 +1,10 @@ +(ns guestbook-datomic.dev-middleware + (:require [ring.middleware.reload :refer [wrap-reload]] + [selmer.middleware :refer [wrap-error-page]] + [prone.middleware :refer [wrap-exceptions]])) + +(defn wrap-dev [handler] + (-> handler + wrap-reload + wrap-error-page + wrap-exceptions)) diff --git a/guestbook-datomic/env/dev/clj/guestbook_datomic/env.clj b/guestbook-datomic/env/dev/clj/guestbook_datomic/env.clj new file mode 100644 index 0000000..9941d67 --- /dev/null +++ b/guestbook-datomic/env/dev/clj/guestbook_datomic/env.clj @@ -0,0 +1,14 @@ +(ns guestbook-datomic.env + (:require [selmer.parser :as parser] + [clojure.tools.logging :as log] + [guestbook-datomic.dev-middleware :refer [wrap-dev]])) + +(def defaults + {:init + (fn [] + (parser/cache-off!) + (log/info "\n-=[guestbook-datomic started successfully using the development profile]=-")) + :stop + (fn [] + (log/info "\n-=[guestbook-datomic has shut down successfully]=-")) + :middleware wrap-dev}) diff --git a/guestbook-datomic/env/dev/clj/guestbook_datomic/figwheel.clj b/guestbook-datomic/env/dev/clj/guestbook_datomic/figwheel.clj new file mode 100644 index 0000000..660f05f --- /dev/null +++ b/guestbook-datomic/env/dev/clj/guestbook_datomic/figwheel.clj @@ -0,0 +1,12 @@ +(ns guestbook-datomic.figwheel + (:require [figwheel-sidecar.repl-api :as ra])) + +(defn start-fw [] + (ra/start-figwheel!)) + +(defn stop-fw [] + (ra/stop-figwheel!)) + +(defn cljs [] + (ra/cljs-repl)) + diff --git a/guestbook-datomic/env/dev/clj/user.clj b/guestbook-datomic/env/dev/clj/user.clj new file mode 100644 index 0000000..5bcca24 --- /dev/null +++ b/guestbook-datomic/env/dev/clj/user.clj @@ -0,0 +1,21 @@ +(ns user + (:require [guestbook-datomic.config :refer [env]] + [clojure.spec.alpha :as s] + [expound.alpha :as expound] + [mount.core :as mount] + [guestbook-datomic.figwheel :refer [start-fw stop-fw cljs]] + [guestbook-datomic.core :refer [start-app]])) + +(alter-var-root #'s/*explain-out* (constantly expound/printer)) + +(defn start [] + (mount/start-without #'guestbook-datomic.core/repl-server)) + +(defn stop [] + (mount/stop-except #'guestbook-datomic.core/repl-server)) + +(defn restart [] + (stop) + (start)) + + diff --git a/guestbook-datomic/env/dev/cljs/guestbook_datomic/app.cljs b/guestbook-datomic/env/dev/cljs/guestbook_datomic/app.cljs new file mode 100644 index 0000000..8c4b212 --- /dev/null +++ b/guestbook-datomic/env/dev/cljs/guestbook_datomic/app.cljs @@ -0,0 +1,13 @@ +(ns ^:figwheel-no-load guestbook-datomic.app + (:require [guestbook-datomic.core :as core] + [cljs.spec.alpha :as s] + [expound.alpha :as expound] + [devtools.core :as devtools])) + +(set! s/*explain-out* expound/printer) + +(enable-console-print!) + +(devtools/install!) + +(core/init!) diff --git a/guestbook-datomic/env/dev/resources/config.edn b/guestbook-datomic/env/dev/resources/config.edn new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/guestbook-datomic/env/dev/resources/config.edn @@ -0,0 +1 @@ +{} diff --git a/guestbook-datomic/env/dev/resources/logback.xml b/guestbook-datomic/env/dev/resources/logback.xml new file mode 100644 index 0000000..e8dcde8 --- /dev/null +++ b/guestbook-datomic/env/dev/resources/logback.xml @@ -0,0 +1,35 @@ + + + + + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + log/guestbook-datomic.log + + log/guestbook-datomic.%d{yyyy-MM-dd}.%i.log + + 100MB + + + 30 + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + + + + + + + + diff --git a/guestbook-datomic/env/prod/clj/guestbook_datomic/env.clj b/guestbook-datomic/env/prod/clj/guestbook_datomic/env.clj new file mode 100644 index 0000000..3104c2b --- /dev/null +++ b/guestbook-datomic/env/prod/clj/guestbook_datomic/env.clj @@ -0,0 +1,11 @@ +(ns guestbook-datomic.env + (:require [clojure.tools.logging :as log])) + +(def defaults + {:init + (fn [] + (log/info "\n-=[guestbook-datomic started successfully]=-")) + :stop + (fn [] + (log/info "\n-=[guestbook-datomic has shut down successfully]=-")) + :middleware identity}) diff --git a/guestbook-datomic/env/prod/cljs/guestbook_datomic/app.cljs b/guestbook-datomic/env/prod/cljs/guestbook_datomic/app.cljs new file mode 100644 index 0000000..134bd77 --- /dev/null +++ b/guestbook-datomic/env/prod/cljs/guestbook_datomic/app.cljs @@ -0,0 +1,7 @@ +(ns guestbook-datomic.app + (:require [guestbook-datomic.core :as core])) + +;;ignore println statements in prod +(set! *print-fn* (fn [& _])) + +(core/init!) diff --git a/guestbook-datomic/env/prod/resources/config.edn b/guestbook-datomic/env/prod/resources/config.edn new file mode 100644 index 0000000..e24ec21 --- /dev/null +++ b/guestbook-datomic/env/prod/resources/config.edn @@ -0,0 +1,2 @@ +{:prod true + :port 3000} diff --git a/guestbook-datomic/env/prod/resources/logback.xml b/guestbook-datomic/env/prod/resources/logback.xml new file mode 100644 index 0000000..661d1d0 --- /dev/null +++ b/guestbook-datomic/env/prod/resources/logback.xml @@ -0,0 +1,24 @@ + + + + + log/guestbook-datomic.log + + log/guestbook-datomic.%d{yyyy-MM-dd}.%i.log + + 100MB + + + 30 + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + + + + + diff --git a/guestbook-datomic/env/test/resources/config.edn b/guestbook-datomic/env/test/resources/config.edn new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/guestbook-datomic/env/test/resources/config.edn @@ -0,0 +1 @@ +{} diff --git a/guestbook-datomic/env/test/resources/logback.xml b/guestbook-datomic/env/test/resources/logback.xml new file mode 100644 index 0000000..e8dcde8 --- /dev/null +++ b/guestbook-datomic/env/test/resources/logback.xml @@ -0,0 +1,35 @@ + + + + + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + log/guestbook-datomic.log + + log/guestbook-datomic.%d{yyyy-MM-dd}.%i.log + + 100MB + + + 30 + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + + + + + + + + diff --git a/guestbook-datomic/figwheel_server.log b/guestbook-datomic/figwheel_server.log new file mode 100644 index 0000000..e69de29 diff --git a/guestbook-datomic/project.clj b/guestbook-datomic/project.clj new file mode 100644 index 0000000..be903e1 --- /dev/null +++ b/guestbook-datomic/project.clj @@ -0,0 +1,134 @@ +(defproject guestbook-datomic "0.1.0-SNAPSHOT" + + :description "FIXME: write description" + :url "http://example.com/FIXME" + + :dependencies [[clj-time "0.14.4"] + [cljs-ajax "0.7.3"] + [com.datomic/datomic-free "0.9.5561" :exclusions [org.slf4j/log4j-over-slf4j org.slf4j/slf4j-nop com.google.guava/guava]] + [com.google.guava/guava "25.1-jre"] + [compojure "1.6.1"] + [cprop "0.1.11"] + [funcool/struct "1.3.0"] + [luminus-immutant "0.2.4"] + [luminus-nrepl "0.1.4"] + [luminus/ring-ttl-session "0.3.2"] + [markdown-clj "1.0.2"] + [metosin/muuntaja "0.5.0"] + [metosin/ring-http-response "0.9.0"] + [mount "0.1.12"] + [org.clojure/clojure "1.9.0"] + [org.clojure/clojurescript "1.10.238" :scope "provided"] + [org.clojure/tools.cli "0.3.7"] + [org.clojure/tools.logging "0.4.1"] + [org.webjars.bower/tether "1.4.4"] + [org.webjars/bootstrap "4.1.0"] + [org.webjars/font-awesome "5.0.13"] + [org.webjars/webjars-locator "0.34"] + [re-frame "0.10.5"] + [reagent "0.8.1"] + [ring-webjars "0.2.0"] + [ring/ring-core "1.6.3"] + [ring/ring-defaults "0.3.2"] + [secretary "1.2.3"] + [selmer "1.11.7"]] + + :min-lein-version "2.0.0" + + :source-paths ["src/clj" "src/cljs" "src/cljc"] + :test-paths ["test/clj"] + :resource-paths ["resources" "target/cljsbuild"] + :target-path "target/%s/" + :main ^:skip-aot guestbook-datomic.core + + :plugins [[lein-cljsbuild "1.1.5"] + [lein-immutant "2.1.0"]] + :clean-targets ^{:protect false} + [:target-path [:cljsbuild :builds :app :compiler :output-dir] [:cljsbuild :builds :app :compiler :output-to]] + :figwheel + {:http-server-root "public" + :nrepl-port 7002 + :css-dirs ["resources/public/css"] + :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]} + + + :profiles + {:uberjar {:omit-source true + :prep-tasks ["compile" ["cljsbuild" "once" "min"]] + :cljsbuild + {:builds + {:min + {:source-paths ["src/cljc" "src/cljs" "env/prod/cljs"] + :compiler + {:output-dir "target/cljsbuild/public/js" + :output-to "target/cljsbuild/public/js/app.js" + :source-map "target/cljsbuild/public/js/app.js.map" + :optimizations :advanced + :pretty-print false + :closure-warnings + {:externs-validation :off :non-standard-jsdoc :off} + :externs ["react/externs/react.js"]}}}} + + + :aot :all + :uberjar-name "guestbook-datomic.jar" + :source-paths ["env/prod/clj"] + :resource-paths ["env/prod/resources"]} + + :dev [:project/dev :profiles/dev] + :test [:project/dev :project/test :profiles/test] + + :project/dev {:jvm-opts ["-Dconf=dev-config.edn"] + :dependencies [[binaryage/devtools "0.9.10"] + [com.cemerick/piggieback "0.2.2"] + [day8.re-frame/re-frame-10x "0.3.3-react16"] + [doo "0.1.10"] + [expound "0.7.0"] + [figwheel-sidecar "0.5.16"] + [pjstadig/humane-test-output "0.8.3"] + [prone "1.6.0"] + [ring/ring-devel "1.6.3"] + [ring/ring-mock "0.3.2"]] + :plugins [[com.jakemccrary/lein-test-refresh "0.19.0"] + [lein-doo "0.1.10"] + [lein-figwheel "0.5.16"] + [org.clojure/clojurescript "1.10.238"]] + :cljsbuild + {:builds + {:app + {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"] + :figwheel {:on-jsload "guestbook-datomic.core/mount-components"} + :compiler + {:main "guestbook-datomic.app" + :asset-path "/js/out" + :output-to "target/cljsbuild/public/js/app.js" + :output-dir "target/cljsbuild/public/js/out" + :source-map true + :optimizations :none + :pretty-print true + :closure-defines {"re_frame.trace.trace_enabled_QMARK_" true} + :preloads [day8.re-frame-10x.preload]}}}} + + + + :doo {:build "test"} + :source-paths ["env/dev/clj"] + :resource-paths ["env/dev/resources"] + :repl-options {:init-ns user} + :injections [(require 'pjstadig.humane-test-output) + (pjstadig.humane-test-output/activate!)]} + :project/test {:jvm-opts ["-Dconf=test-config.edn"] + :resource-paths ["env/test/resources"] + :cljsbuild + {:builds + {:test + {:source-paths ["src/cljc" "src/cljs" "test/cljs"] + :compiler + {:output-to "target/test.js" + :main "guestbook-datomic.doo-runner" + :optimizations :whitespace + :pretty-print true}}}} + + } + :profiles/dev {} + :profiles/test {}}) diff --git a/guestbook-datomic/resources/docs/docs.md b/guestbook-datomic/resources/docs/docs.md new file mode 100644 index 0000000..4bb148d --- /dev/null +++ b/guestbook-datomic/resources/docs/docs.md @@ -0,0 +1,82 @@ +

Congratulations, your Luminus site is ready!

+ +This page will help guide you through the first steps of building your site. + +#### Why are you seeing this page? + +The `home-routes` handler in the `guestbook-datomic.routes.home` namespace +defines the route that invokes the `home-page` function whenever an HTTP +request is made to the `/` URI using the `GET` method. + +``` +(defroutes home-routes + (GET "/" [] + (home-page)) + (GET "/docs" [] + (-> (response/ok (-> "docs/docs.md" io/resource slurp)) + (response/header "Content-Type" "text/plain; charset=utf-8")))) +``` + +The `home-page` function will in turn call the `guestbook-datomic.layout/render` function +to render the HTML content: + +``` +(defn home-page [] + (layout/render "home.html")) +``` + +The page contains a link to the compiled ClojureScript found in the `target/cljsbuild/public` folder: + +``` +{% script "/js/app.js" %} +``` + +The rest of this page is rendered by ClojureScript found in the `src/cljs/guestbook_datomic/core.cljs` file. + + + +#### Organizing the routes + +The routes are aggregated and wrapped with middleware in the `guestbook-datomic.handler` namespace: + +``` +(defstate app + :start + (middleware/wrap-base + (routes + (-> #'home-routes + (wrap-routes middleware/wrap-csrf) + (wrap-routes middleware/wrap-formats)) + (route/not-found + (:body + (error-page {:status 404 + :title "page not found"})))))) +``` + +The `app` definition groups all the routes in the application into a single handler. +A default route group is added to handle the `404` case. + +learn more about routing » + +The `home-routes` are wrapped with two middleware functions. The first enables CSRF protection. +The second takes care of serializing and deserializing various encoding formats, such as JSON. + +#### Managing your middleware + +Request middleware functions are located under the `guestbook-datomic.middleware` namespace. + +This namespace is reserved for any custom middleware for the application. Some default middleware is +already defined here. The middleware is assembled in the `wrap-base` function. + +Middleware used for development is placed in the `guestbook-datomic.dev-middleware` namespace found in +the `env/dev/clj/` source path. + +learn more about middleware » + + + + +#### Need some help? + +Visit the [official documentation](http://www.luminusweb.net/docs) for examples +on how to accomplish common tasks with Luminus. The `#luminus` channel on the [Clojurians Slack](http://clojurians.net/) and [Google Group](https://groups.google.com/forum/#!forum/luminusweb) are both great places to seek help and discuss projects with other users. diff --git a/guestbook-datomic/resources/public/css/screen.css b/guestbook-datomic/resources/public/css/screen.css new file mode 100644 index 0000000..3a8307f --- /dev/null +++ b/guestbook-datomic/resources/public/css/screen.css @@ -0,0 +1,69 @@ +html, +body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + height: 100%; +} +.navbar { + margin-bottom: 10px; + border-radius: 0px; +} +.navbar-brand { + float: none; +} +.navbar-nav .nav-item { + float: none; +} +.navbar-divider, +.navbar-nav .nav-item+.nav-item, +.navbar-nav .nav-link + .nav-link { + margin-left: 0; +} +@media (min-width: 34em) { + .navbar-brand { + float: left; + } + .navbar-nav .nav-item { + float: left; + } + .navbar-divider, + .navbar-nav .nav-item+.nav-item, + .navbar-nav .nav-link + .nav-link { + margin-left: 1rem; + } +} + +@-moz-keyframes three-quarters-loader { + 0% { + -moz-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes three-quarters-loader { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes three-quarters-loader { + 0% { + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + diff --git a/guestbook-datomic/resources/public/favicon.ico b/guestbook-datomic/resources/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0e50cb2fb96b2ae63a2cf81adf4e1b3869cfe152 GIT binary patch literal 1150 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 P-(WnP}f5EZ_VG%x##xBtZkUi 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 literal 0 HcmV?d00001 diff --git a/guestbook-datomic/resources/templates/error.html b/guestbook-datomic/resources/templates/error.html new file mode 100644 index 0000000..31dd41d --- /dev/null +++ b/guestbook-datomic/resources/templates/error.html @@ -0,0 +1,55 @@ + + + + Something bad happened + + + {% style "/assets/bootstrap/css/bootstrap.min.css" %} + + + +
+
+
+
+
+

Error: {{status}}

+
+ {% if title %} +

{{title}}

+ {% endif %} + {% if message %} +

{{message}}

+ {% endif %} +
+
+
+
+
+ + diff --git a/guestbook-datomic/resources/templates/home.html b/guestbook-datomic/resources/templates/home.html new file mode 100644 index 0000000..251b999 --- /dev/null +++ b/guestbook-datomic/resources/templates/home.html @@ -0,0 +1,46 @@ + + + + + + Welcome to guestbook-datomic + + + +
+ + + {% style "/assets/bootstrap/css/bootstrap.min.css" %} + {% style "/assets/font-awesome/web-fonts-with-css/css/fontawesome-all.min.css" %} + {% style "/css/screen.css" %} + + {% script "/assets/jquery/jquery.min.js" %} + {% script "/assets/font-awesome/svg-with-js/js/fontawesome.min.js" %} + {% script "/assets/tether/dist/js/tether.min.js" %} + {% script "/assets/bootstrap/js/bootstrap.min.js" %} + + + {% script "/js/app.js" %} + + diff --git a/guestbook-datomic/src/clj/guestbook_datomic/config.clj b/guestbook-datomic/src/clj/guestbook_datomic/config.clj new file mode 100644 index 0000000..e389e53 --- /dev/null +++ b/guestbook-datomic/src/clj/guestbook_datomic/config.clj @@ -0,0 +1,12 @@ +(ns guestbook-datomic.config + (:require [cprop.core :refer [load-config]] + [cprop.source :as source] + [mount.core :refer [args defstate]])) + +(defstate env + :start + (load-config + :merge + [(args) + (source/from-system-props) + (source/from-env)])) diff --git a/guestbook-datomic/src/clj/guestbook_datomic/core.clj b/guestbook-datomic/src/clj/guestbook_datomic/core.clj new file mode 100644 index 0000000..42b2956 --- /dev/null +++ b/guestbook-datomic/src/clj/guestbook_datomic/core.clj @@ -0,0 +1,50 @@ +(ns guestbook-datomic.core + (:require [guestbook-datomic.handler :as handler] + [luminus.repl-server :as repl] + [luminus.http-server :as http] + [guestbook-datomic.config :refer [env]] + [clojure.tools.cli :refer [parse-opts]] + [clojure.tools.logging :as log] + [mount.core :as mount]) + (:gen-class)) + +(def cli-options + [["-p" "--port PORT" "Port number" + :parse-fn #(Integer/parseInt %)]]) + +(mount/defstate ^{:on-reload :noop} http-server + :start + (http/start + (-> env + (assoc :handler #'handler/app) + (update :io-threads #(or % (* 2 (.availableProcessors (Runtime/getRuntime))))) + (update :port #(or (-> env :options :port) %)))) + :stop + (http/stop http-server)) + +(mount/defstate ^{:on-reload :noop} repl-server + :start + (when (env :nrepl-port) + (repl/start { :bind (env :nrepl-bind) + :port (env :nrepl-port) + })) + :stop + (when repl-server + (repl/stop repl-server))) + + +(defn stop-app [] + (doseq [component (:stopped (mount/stop))] + (log/info component "stopped")) + (shutdown-agents)) + +(defn start-app [args] + (doseq [component (-> args + (parse-opts cli-options) + mount/start-with-args + :started)] + (log/info component "started")) + (.addShutdownHook (Runtime/getRuntime) (Thread. stop-app))) + +(defn -main [& args] + (start-app args)) diff --git a/guestbook-datomic/src/clj/guestbook_datomic/db/core.clj b/guestbook-datomic/src/clj/guestbook_datomic/db/core.clj new file mode 100644 index 0000000..626a139 --- /dev/null +++ b/guestbook-datomic/src/clj/guestbook_datomic/db/core.clj @@ -0,0 +1,48 @@ +(ns guestbook-datomic.db.core + (:require [datomic.api :as d] + [mount.core :refer [defstate]] + [guestbook-datomic.config :refer [env]])) + +(defstate conn + :start (-> env :database-url d/connect) + :stop (-> conn .release)) + +(defn create-schema [] + (let [schema [{:db/ident :user/id + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db.install/_attribute :db.part/db} + {:db/ident :user/first-name + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db.install/_attribute :db.part/db} + {:db/ident :user/last-name + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db.install/_attribute :db.part/db} + {:db/ident :user/email + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db.install/_attribute :db.part/db}]] + @(d/transact conn schema))) + +(defn entity [conn id] + (d/entity (d/db conn) id)) + +(defn touch [conn results] + "takes 'entity ids' results from a query + e.g. '#{[272678883689461] [272678883689462] [272678883689459] [272678883689457]}'" + (let [e (partial entity conn)] + (map #(-> % first e d/touch) results))) + +(defn add-user [conn {:keys [id first-name last-name email]}] + @(d/transact conn [{:db/id id + :user/first-name first-name + :user/last-name last-name + :user/email email}])) + +(defn find-user [conn id] + (let [user (d/q '[:find ?e :in $ ?id + :where [?e :user/id ?id]] + (d/db conn) id)] + (touch conn user))) diff --git a/guestbook-datomic/src/clj/guestbook_datomic/handler.clj b/guestbook-datomic/src/clj/guestbook_datomic/handler.clj new file mode 100644 index 0000000..869b191 --- /dev/null +++ b/guestbook-datomic/src/clj/guestbook_datomic/handler.clj @@ -0,0 +1,26 @@ +(ns guestbook-datomic.handler + (:require + [guestbook-datomic.layout :refer [error-page]] + [guestbook-datomic.routes.home :refer [home-routes]] + [compojure.core :refer [routes wrap-routes]] + [compojure.route :as route] + [guestbook-datomic.env :refer [defaults]] + [mount.core :as mount] + [guestbook-datomic.middleware :as middleware])) + +(mount/defstate init-app + :start ((or (:init defaults) identity)) + :stop ((or (:stop defaults) identity))) + +(mount/defstate app + :start + (middleware/wrap-base + (routes + (-> #'home-routes + (wrap-routes middleware/wrap-csrf) + (wrap-routes middleware/wrap-formats)) + (route/not-found + (:body + (error-page {:status 404 + :title "page not found"})))))) + diff --git a/guestbook-datomic/src/clj/guestbook_datomic/layout.clj b/guestbook-datomic/src/clj/guestbook_datomic/layout.clj new file mode 100644 index 0000000..33d1908 --- /dev/null +++ b/guestbook-datomic/src/clj/guestbook_datomic/layout.clj @@ -0,0 +1,37 @@ +(ns guestbook-datomic.layout + (:require [selmer.parser :as parser] + [selmer.filters :as filters] + [markdown.core :refer [md-to-html-string]] + [ring.util.http-response :refer [content-type ok]] + [ring.util.anti-forgery :refer [anti-forgery-field]] + [ring.middleware.anti-forgery :refer [*anti-forgery-token*]])) + + +(parser/set-resource-path! (clojure.java.io/resource "templates")) +(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field))) +(filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)])) + +(defn render + "renders the HTML template located relative to resources/templates" + [template & [params]] + (content-type + (ok + (parser/render-file + template + (assoc params + :page template + :csrf-token *anti-forgery-token*))) + "text/html; charset=utf-8")) + +(defn error-page + "error-details should be a map containing the following keys: + :status - error status + :title - error title (optional) + :message - detailed error message (optional) + + returns a response map with the error page as the body + and the status specified by the status key" + [error-details] + {:status (:status error-details) + :headers {"Content-Type" "text/html; charset=utf-8"} + :body (parser/render-file "error.html" error-details)}) diff --git a/guestbook-datomic/src/clj/guestbook_datomic/middleware.clj b/guestbook-datomic/src/clj/guestbook_datomic/middleware.clj new file mode 100644 index 0000000..21de24d --- /dev/null +++ b/guestbook-datomic/src/clj/guestbook_datomic/middleware.clj @@ -0,0 +1,81 @@ +(ns guestbook-datomic.middleware + (:require [guestbook-datomic.env :refer [defaults]] + [cheshire.generate :as cheshire] + [cognitect.transit :as transit] + [clojure.tools.logging :as log] + [guestbook-datomic.layout :refer [error-page]] + [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] + [ring.middleware.webjars :refer [wrap-webjars]] + [muuntaja.core :as muuntaja] + [muuntaja.format.json :refer [json-format]] + [muuntaja.format.transit :as transit-format] + [muuntaja.middleware :refer [wrap-format wrap-params]] + [guestbook-datomic.config :refer [env]] + [ring.middleware.flash :refer [wrap-flash]] + [immutant.web.middleware :refer [wrap-session]] + [ring.middleware.defaults :refer [site-defaults wrap-defaults]]) + (:import [javax.servlet ServletContext] + [org.joda.time ReadableInstant])) + +(defn wrap-internal-error [handler] + (fn [req] + (try + (handler req) + (catch Throwable t + (log/error t (.getMessage t)) + (error-page {:status 500 + :title "Something very bad has happened!" + :message "We've dispatched a team of highly trained gnomes to take care of the problem."}))))) + +(defn wrap-csrf [handler] + (wrap-anti-forgery + handler + {:error-response + (error-page + {:status 403 + :title "Invalid anti-forgery token"})})) + +(def joda-time-writer + (transit/write-handler + (constantly "m") + (fn [v] (-> ^ReadableInstant v .getMillis)) + (fn [v] (-> ^ReadableInstant v .getMillis .toString)))) + +(cheshire/add-encoder + org.joda.time.DateTime + (fn [c jsonGenerator] + (.writeString jsonGenerator (-> ^ReadableInstant c .getMillis .toString)))) + +(def restful-format-options + (update + muuntaja/default-options + :formats + merge + {"application/json" + json-format + + "application/transit+json" + {:decoder [(partial transit-format/make-transit-decoder :json)] + :encoder [#(transit-format/make-transit-encoder + :json + (merge + % + {:handlers {org.joda.time.DateTime joda-time-writer}}))]}})) + +(defn wrap-formats [handler] + (let [wrapped (-> handler wrap-params (wrap-format restful-format-options))] + (fn [request] + ;; disable wrap-formats for websockets + ;; since they're not compatible with this middleware + ((if (:websocket? request) handler wrapped) request)))) + +(defn wrap-base [handler] + (-> ((:middleware defaults) handler) + wrap-webjars + wrap-flash + (wrap-session {:cookie-attrs {:http-only true}}) + (wrap-defaults + (-> site-defaults + (assoc-in [:security :anti-forgery] false) + (dissoc :session))) + wrap-internal-error)) diff --git a/guestbook-datomic/src/clj/guestbook_datomic/routes/home.clj b/guestbook-datomic/src/clj/guestbook_datomic/routes/home.clj new file mode 100644 index 0000000..e2f7d24 --- /dev/null +++ b/guestbook-datomic/src/clj/guestbook_datomic/routes/home.clj @@ -0,0 +1,16 @@ +(ns guestbook-datomic.routes.home + (:require [guestbook-datomic.layout :as layout] + [compojure.core :refer [defroutes GET]] + [ring.util.http-response :as response] + [clojure.java.io :as io])) + +(defn home-page [] + (layout/render "home.html")) + +(defroutes home-routes + (GET "/" [] + (home-page)) + (GET "/docs" [] + (-> (response/ok (-> "docs/docs.md" io/resource slurp)) + (response/header "Content-Type" "text/plain; charset=utf-8")))) + diff --git a/guestbook-datomic/src/cljc/guestbook_datomic/validation.cljc b/guestbook-datomic/src/cljc/guestbook_datomic/validation.cljc new file mode 100644 index 0000000..930a06f --- /dev/null +++ b/guestbook-datomic/src/cljc/guestbook_datomic/validation.cljc @@ -0,0 +1,2 @@ +(ns guestbook-datomic.validation + (:require [struct.core :as st])) diff --git a/guestbook-datomic/src/cljs/guestbook_datomic/ajax.cljs b/guestbook-datomic/src/cljs/guestbook_datomic/ajax.cljs new file mode 100644 index 0000000..a08c7a8 --- /dev/null +++ b/guestbook-datomic/src/cljs/guestbook_datomic/ajax.cljs @@ -0,0 +1,19 @@ +(ns guestbook-datomic.ajax + (:require [ajax.core :as ajax])) + +(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)) + +(defn load-interceptors! [] + (swap! ajax/default-interceptors + conj + (ajax/to-interceptor {:name "default headers" + :request default-headers}))) + + diff --git a/guestbook-datomic/src/cljs/guestbook_datomic/core.cljs b/guestbook-datomic/src/cljs/guestbook_datomic/core.cljs new file mode 100644 index 0000000..4e0abb6 --- /dev/null +++ b/guestbook-datomic/src/cljs/guestbook_datomic/core.cljs @@ -0,0 +1,93 @@ +(ns guestbook-datomic.core + (:require [reagent.core :as r] + [re-frame.core :as rf] + + [goog.events :as events] + [goog.history.EventType :as HistoryEventType] + [markdown.core :refer [md->html]] + [ajax.core :refer [GET POST]] + [guestbook-datomic.ajax :refer [load-interceptors!]] + [guestbook-datomic.events] + [secretary.core :as secretary]) + (:import goog.History)) + +(defn nav-link [uri title page] + [:li.nav-item + {:class (when (= page @(rf/subscribe [:page])) "active")} + [:a.nav-link {:href uri} title]]) + +(defn navbar [] + [:nav.navbar.navbar-dark.bg-primary.navbar-expand-md + {:role "navigation"} + [:button.navbar-toggler.hidden-sm-up + {:type "button" + :data-toggle "collapse" + :data-target "#collapsing-navbar"} + [:span.navbar-toggler-icon]] + [:a.navbar-brand {:href "#/"} "guestbook-datomic"] + [:div#collapsing-navbar.collapse.navbar-collapse + [:ul.nav.navbar-nav.mr-auto + [nav-link "#/" "Home" :home] + [nav-link "#/about" "About" :about]]]]) + +(defn about-page [] + [:div.container + [:div.row + [:div.col-md-12 + [:img {:src "/img/warning_clojure.png"}]]]]) + +(defn home-page [] + [:div.container + [:div.row>div.col-sm-12 + [:h2.alert.alert-info "Tip: try pressing CTRL+H to open re-frame tracing menu"]] + (when-let [docs @(rf/subscribe [:docs])] + [:div.row>div.col-sm-12 + [:div {:dangerouslySetInnerHTML + {:__html (md->html docs)}}]])]) + +(def pages + {:home #'home-page + :about #'about-page}) + +(defn page [] + [:div + [navbar] + [(pages @(rf/subscribe [:page]))]]) + +;; ------------------------- +;; Routes + +(secretary/set-config! :prefix "#") + +(secretary/defroute "/" [] + (rf/dispatch [:navigate :home])) + +(secretary/defroute "/about" [] + (rf/dispatch [:navigate :about])) + +;; ------------------------- +;; History +;; must be called after routes have been defined +(defn hook-browser-navigation! [] + (doto (History.) + (events/listen + HistoryEventType/NAVIGATE + (fn [event] + (secretary/dispatch! (.-token event)))) + (.setEnabled true))) + +;; ------------------------- +;; Initialize app +(defn fetch-docs! [] + (GET "/docs" {:handler #(rf/dispatch [:set-docs %])})) + +(defn mount-components [] + (rf/clear-subscription-cache!) + (r/render [#'page] (.getElementById js/document "app"))) + +(defn init! [] + (rf/dispatch-sync [:navigate :home]) + (load-interceptors!) + (fetch-docs!) + (hook-browser-navigation!) + (mount-components)) diff --git a/guestbook-datomic/src/cljs/guestbook_datomic/events.cljs b/guestbook-datomic/src/cljs/guestbook_datomic/events.cljs new file mode 100644 index 0000000..4897db9 --- /dev/null +++ b/guestbook-datomic/src/cljs/guestbook_datomic/events.cljs @@ -0,0 +1,26 @@ +(ns guestbook-datomic.events + (:require [re-frame.core :refer [dispatch reg-event-db reg-sub]])) + +;;dispatchers + +(reg-event-db + :navigate + (fn [db [_ page]] + (assoc db :page page))) + +(reg-event-db + :set-docs + (fn [db [_ docs]] + (assoc db :docs docs))) + +;;subscriptions + +(reg-sub + :page + (fn [db _] + (:page db))) + +(reg-sub + :docs + (fn [db _] + (:docs db))) diff --git a/guestbook-datomic/test/clj/guestbook_datomic/test/handler.clj b/guestbook-datomic/test/clj/guestbook_datomic/test/handler.clj new file mode 100644 index 0000000..7e65b7c --- /dev/null +++ b/guestbook-datomic/test/clj/guestbook_datomic/test/handler.clj @@ -0,0 +1,21 @@ +(ns guestbook-datomic.test.handler + (:require [clojure.test :refer :all] + [ring.mock.request :refer :all] + [guestbook-datomic.handler :refer :all] + [mount.core :as mount])) + +(use-fixtures + :once + (fn [f] + (mount/start #'guestbook-datomic.config/env + #'guestbook-datomic.handler/app) + (f))) + +(deftest test-app + (testing "main route" + (let [response (app (request :get "/"))] + (is (= 200 (:status response))))) + + (testing "not-found route" + (let [response (app (request :get "/invalid"))] + (is (= 404 (:status response)))))) diff --git a/guestbook-datomic/test/cljs/guestbook_datomic/core_test.cljs b/guestbook-datomic/test/cljs/guestbook_datomic/core_test.cljs new file mode 100644 index 0000000..5007d19 --- /dev/null +++ b/guestbook-datomic/test/cljs/guestbook_datomic/core_test.cljs @@ -0,0 +1,9 @@ +(ns guestbook-datomic.core-test + (:require [cljs.test :refer-macros [is are deftest testing use-fixtures]] + [pjstadig.humane-test-output] + [reagent.core :as reagent :refer [atom]] + [guestbook-datomic.core :as rc])) + +(deftest test-home + (is (= true true))) + diff --git a/guestbook-datomic/test/cljs/guestbook_datomic/doo_runner.cljs b/guestbook-datomic/test/cljs/guestbook_datomic/doo_runner.cljs new file mode 100644 index 0000000..25aca2a --- /dev/null +++ b/guestbook-datomic/test/cljs/guestbook_datomic/doo_runner.cljs @@ -0,0 +1,6 @@ +(ns guestbook-datomic.doo-runner + (:require [doo.runner :refer-macros [doo-tests]] + [guestbook-datomic.core-test])) + +(doo-tests 'guestbook-datomic.core-test) + From e299d5e5b1be2901c53c1915f8160010091cb4ac Mon Sep 17 00:00:00 2001 From: "Niluka Satharasinghe (folcon)" Date: Sat, 9 Jun 2018 17:38:42 +0100 Subject: [PATCH 2/5] Added some info about lein figwheel --- guestbook-datomic/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/guestbook-datomic/README.md b/guestbook-datomic/README.md index de672ba..a635ec9 100644 --- a/guestbook-datomic/README.md +++ b/guestbook-datomic/README.md @@ -14,7 +14,11 @@ You will need [Leiningen][1] 2.0 or above installed. To start a web server for the application, run: - lein run + lein run + +Then in a second terminal run: + + lein figwheel ## License From 552e9edfacc121f31de2ff0b8a900e23aa8a6f4e Mon Sep 17 00:00:00 2001 From: "Niluka Satharasinghe (folcon)" Date: Sat, 9 Jun 2018 17:41:26 +0100 Subject: [PATCH 3/5] Adding examples of config --- guestbook-datomic/dev-config.edn | 14 ++++++++++++++ guestbook-datomic/test-config.edn | 10 ++++++++++ 2 files changed, 24 insertions(+) create mode 100644 guestbook-datomic/dev-config.edn create mode 100644 guestbook-datomic/test-config.edn diff --git a/guestbook-datomic/dev-config.edn b/guestbook-datomic/dev-config.edn new file mode 100644 index 0000000..ad499d1 --- /dev/null +++ b/guestbook-datomic/dev-config.edn @@ -0,0 +1,14 @@ +;; WARNING +;; The dev-config.edn file is used for local environment variables, such as database credentials. +;; This file is listed in .gitignore and will be excluded from version control by Git. + +{:dev true + :port 3000 + ;; when :nrepl-port is set the application starts the nREPL server on load + :nrepl-port 7000 + + ; set your dev database connection URL here + ; :database-url "datomic:free://localhost:4334/guestbook_datomic_dev" + ; Don't forget you can also use the datomic mem db which can be useful when developing + ; :database-url "datomic:mem://guestbook_datomic_dev" +} diff --git a/guestbook-datomic/test-config.edn b/guestbook-datomic/test-config.edn new file mode 100644 index 0000000..c080f7d --- /dev/null +++ b/guestbook-datomic/test-config.edn @@ -0,0 +1,10 @@ +;; WARNING +;; The test-config.edn file is used for local environment variables, such as database credentials. +;; This file is listed in .gitignore and will be excluded from version control by Git. + +{:port 3000 + ; set your test database connection URL here + ; :database-url "datomic:free://localhost:4334/guestbook_datomic_test" + ; Don't forget you can also use the datomic mem db which can be useful when developing + ; :database-url "datomic:mem://guestbook_datomic_test" +} From 0a259cb189e7768e80974ac447364e5068908ad7 Mon Sep 17 00:00:00 2001 From: "Niluka Satharasinghe (folcon)" Date: Sat, 9 Jun 2018 18:29:11 +0100 Subject: [PATCH 4/5] Activate config so it can be used --- guestbook-datomic/dev-config.edn | 2 +- guestbook-datomic/test-config.edn | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/guestbook-datomic/dev-config.edn b/guestbook-datomic/dev-config.edn index ad499d1..c590438 100644 --- a/guestbook-datomic/dev-config.edn +++ b/guestbook-datomic/dev-config.edn @@ -10,5 +10,5 @@ ; set your dev database connection URL here ; :database-url "datomic:free://localhost:4334/guestbook_datomic_dev" ; Don't forget you can also use the datomic mem db which can be useful when developing - ; :database-url "datomic:mem://guestbook_datomic_dev" + :database-url "datomic:mem://guestbook_datomic_dev" } diff --git a/guestbook-datomic/test-config.edn b/guestbook-datomic/test-config.edn index c080f7d..4f1d9ce 100644 --- a/guestbook-datomic/test-config.edn +++ b/guestbook-datomic/test-config.edn @@ -6,5 +6,5 @@ ; set your test database connection URL here ; :database-url "datomic:free://localhost:4334/guestbook_datomic_test" ; Don't forget you can also use the datomic mem db which can be useful when developing - ; :database-url "datomic:mem://guestbook_datomic_test" + :database-url "datomic:mem://guestbook_datomic_test" } From 629001702942fb23e6c0a88ab7ea0db07440b418 Mon Sep 17 00:00:00 2001 From: "Niluka Satharasinghe (folcon)" Date: Sun, 10 Jun 2018 19:33:25 +0100 Subject: [PATCH 5/5] First pass attempt at implementing complete example --- guestbook-datomic/project.clj | 1 + .../resources/public/css/screen.css | 14 +++++ .../src/clj/guestbook_datomic/db/core.clj | 60 +++++++++++++++--- .../src/clj/guestbook_datomic/routes/home.clj | 20 ++++-- .../cljc/guestbook_datomic/validation.cljc | 14 +++++ .../src/cljs/guestbook_datomic/core.cljs | 58 +++++++++++++++--- .../src/cljs/guestbook_datomic/events.cljs | 61 ++++++++++++++++++- 7 files changed, 202 insertions(+), 26 deletions(-) diff --git a/guestbook-datomic/project.clj b/guestbook-datomic/project.clj index be903e1..0cf72f3 100644 --- a/guestbook-datomic/project.clj +++ b/guestbook-datomic/project.clj @@ -26,6 +26,7 @@ [org.webjars/font-awesome "5.0.13"] [org.webjars/webjars-locator "0.34"] [re-frame "0.10.5"] + [day8.re-frame/http-fx "0.1.6"] [reagent "0.8.1"] [ring-webjars "0.2.0"] [ring/ring-core "1.6.3"] diff --git a/guestbook-datomic/resources/public/css/screen.css b/guestbook-datomic/resources/public/css/screen.css index 3a8307f..fe4b260 100644 --- a/guestbook-datomic/resources/public/css/screen.css +++ b/guestbook-datomic/resources/public/css/screen.css @@ -67,3 +67,17 @@ body { } } +.fa.spinning { + animation: spin 1s infinite linear; + -webkit-animation: spin2 1s infinite linear; +} + +@keyframes spin { + from { transform: scale(1) rotate(0deg); } + to { transform: scale(1) rotate(360deg); } +} + +@-webkit-keyframes spin2 { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(360deg); } +} diff --git a/guestbook-datomic/src/clj/guestbook_datomic/db/core.clj b/guestbook-datomic/src/clj/guestbook_datomic/db/core.clj index 626a139..377164e 100644 --- a/guestbook-datomic/src/clj/guestbook_datomic/db/core.clj +++ b/guestbook-datomic/src/clj/guestbook_datomic/db/core.clj @@ -1,15 +1,13 @@ (ns guestbook-datomic.db.core (:require [datomic.api :as d] [mount.core :refer [defstate]] - [guestbook-datomic.config :refer [env]])) + [guestbook-datomic.config :refer [env]] + [clojure.tools.logging :as log])) -(defstate conn - :start (-> env :database-url d/connect) - :stop (-> conn .release)) - -(defn create-schema [] - (let [schema [{:db/ident :user/id - :db/valueType :db.type/string +(defn create-schema [conn] + (let [schema [;; Users + {:db/ident :user/id + :db/valueType :db.type/uuid :db/cardinality :db.cardinality/one :db.install/_attribute :db.part/db} {:db/ident :user/first-name @@ -23,9 +21,39 @@ {:db/ident :user/email :db/valueType :db.type/string :db/cardinality :db.cardinality/one + :db.install/_attribute :db.part/db} + + ;; Messages + {:db/ident :message/id + :db/valueType :db.type/uuid + :db/cardinality :db.cardinality/one + :db.install/_attribute :db.part/db} + {:db/ident :message/name + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db.install/_attribute :db.part/db} + {:db/ident :message/message + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db.install/_attribute :db.part/db} + {:db/ident :message/timestamp + :db/valueType :db.type/instant + :db/cardinality :db.cardinality/one :db.install/_attribute :db.part/db}]] @(d/transact conn schema))) +(defn init-db [db-url] + (let [;; Ensure database exists + _ (log/info "Created datomic db?" + (d/create-database db-url)) + conn (d/connect db-url)] + (create-schema conn) + conn)) + +(defstate conn + :start (-> env :database-url init-db) + :stop (-> conn .release)) + (defn entity [conn id] (d/entity (d/db conn) id)) @@ -35,8 +63,8 @@ (let [e (partial entity conn)] (map #(-> % first e d/touch) results))) -(defn add-user [conn {:keys [id first-name last-name email]}] - @(d/transact conn [{:db/id id +(defn add-user [conn {:keys [first-name last-name email]}] + @(d/transact conn [{:user/id (java.util.UUID/randomUUID) :user/first-name first-name :user/last-name last-name :user/email email}])) @@ -46,3 +74,15 @@ :where [?e :user/id ?id]] (d/db conn) id)] (touch conn user))) + + +(defn add-message [conn {:keys [name message]}] + @(d/transact conn [{:message/id (java.util.UUID/randomUUID) + :message/name name + :message/message message + :message/timestamp (java.util.Date.)}])) + +(defn get-messages [conn] + (d/q '[:find [(pull ?e [*]) ...] + :where [?e :message/id _ ?tx]] + (d/db conn))) diff --git a/guestbook-datomic/src/clj/guestbook_datomic/routes/home.clj b/guestbook-datomic/src/clj/guestbook_datomic/routes/home.clj index e2f7d24..a1f4e60 100644 --- a/guestbook-datomic/src/clj/guestbook_datomic/routes/home.clj +++ b/guestbook-datomic/src/clj/guestbook_datomic/routes/home.clj @@ -1,16 +1,26 @@ (ns guestbook-datomic.routes.home (:require [guestbook-datomic.layout :as layout] - [compojure.core :refer [defroutes GET]] + [guestbook-datomic.db.core :as db] + [guestbook-datomic.validation :refer [validate-message]] + [compojure.core :refer [defroutes GET POST]] [ring.util.http-response :as response] [clojure.java.io :as io])) (defn home-page [] (layout/render "home.html")) +(defn save-message! [{:keys [params]}] + (if-let [errors (validate-message params)] + (response/bad-request {:errors errors}) + (try + (db/add-message db/conn params) + (response/ok {:status :ok}) + (catch Exception e + (response/internal-server-error + {:errors {:server-error ["Failed to save message!"]}}))))) + (defroutes home-routes (GET "/" [] (home-page)) - (GET "/docs" [] - (-> (response/ok (-> "docs/docs.md" io/resource slurp)) - (response/header "Content-Type" "text/plain; charset=utf-8")))) - + (GET "/messages" [] (response/ok (db/get-messages db/conn))) + (POST "/message" req (save-message! req))) diff --git a/guestbook-datomic/src/cljc/guestbook_datomic/validation.cljc b/guestbook-datomic/src/cljc/guestbook_datomic/validation.cljc index 930a06f..7e74574 100644 --- a/guestbook-datomic/src/cljc/guestbook_datomic/validation.cljc +++ b/guestbook-datomic/src/cljc/guestbook_datomic/validation.cljc @@ -1,2 +1,16 @@ (ns guestbook-datomic.validation (:require [struct.core :as st])) + +(def message-schema + [[:name + st/required + st/string] + + [:message + st/required + st/string + {:message "message must contain at least 10 characters" + :validate #(> (count %) 9)}]]) + +(defn validate-message [params] + (first (st/validate params message-schema))) diff --git a/guestbook-datomic/src/cljs/guestbook_datomic/core.cljs b/guestbook-datomic/src/cljs/guestbook_datomic/core.cljs index 4e0abb6..6fe9f6d 100644 --- a/guestbook-datomic/src/cljs/guestbook_datomic/core.cljs +++ b/guestbook-datomic/src/cljs/guestbook_datomic/core.cljs @@ -8,7 +8,8 @@ [ajax.core :refer [GET POST]] [guestbook-datomic.ajax :refer [load-interceptors!]] [guestbook-datomic.events] - [secretary.core :as secretary]) + [secretary.core :as secretary] + [guestbook-datomic.validation :refer [validate-message]]) (:import goog.History)) (defn nav-link [uri title page] @@ -36,14 +37,58 @@ [:div.col-md-12 [:img {:src "/img/warning_clojure.png"}]]]]) +(defn errors-component [errors id] + (when-let [error (id @errors)] + [:div.alert.alert-danger (clojure.string/join error)])) + +(defn message-form [] + (let [fields (r/atom {}) + errors (r/atom nil)] + (fn [] + [:div.content + [:div.form-group + [errors-component errors :name] + [:p "Name:" + [:input.form-control + {:type :text + :name :name + :on-change #(swap! fields assoc :name (-> % .-target .-value)) + :value (:name @fields)}]] + [errors-component errors :message] + [:p "Message:" + [:textarea.form-control + {:rows 4 + :cols 50 + :name :message + :value (:message @fields) + :on-change #(swap! fields assoc :message (-> % .-target .-value))}]] + [:input.btn.btn-primary + {:type :submit + :on-click #(if-not (validate-message @fields) + (do (reset! errors nil) + (rf/dispatch [:send-message @fields])) + (reset! errors (validate-message @fields))) + :value "comment"}]]]))) + (defn home-page [] [:div.container [:div.row>div.col-sm-12 [:h2.alert.alert-info "Tip: try pressing CTRL+H to open re-frame tracing menu"]] - (when-let [docs @(rf/subscribe [:docs])] + [:div.row>div.col-sm-12 + (when @(rf/subscribe [:show-twirly]) + [:span.fa.fa-spinner.spinning])] + (when-let [messages @(rf/subscribe [:messages])] [:div.row>div.col-sm-12 - [:div {:dangerouslySetInnerHTML - {:__html (md->html docs)}}]])]) + [:ul.content + (for [{:keys [message/id message/timestamp message/message message/name] :as msg} messages] + ^{:key (or id message)} + [:li + (when timestamp + [:time (.toLocaleString (js/Date. timestamp))]) + [:p message] + [:p " - " name]])]]) + [:div.row>div.col-sm-12 + [message-form]]]) (def pages {:home #'home-page @@ -78,9 +123,6 @@ ;; ------------------------- ;; Initialize app -(defn fetch-docs! [] - (GET "/docs" {:handler #(rf/dispatch [:set-docs %])})) - (defn mount-components [] (rf/clear-subscription-cache!) (r/render [#'page] (.getElementById js/document "app"))) @@ -88,6 +130,6 @@ (defn init! [] (rf/dispatch-sync [:navigate :home]) (load-interceptors!) - (fetch-docs!) + (rf/dispatch [:fetch-messages]) (hook-browser-navigation!) (mount-components)) diff --git a/guestbook-datomic/src/cljs/guestbook_datomic/events.cljs b/guestbook-datomic/src/cljs/guestbook_datomic/events.cljs index 4897db9..17ea4af 100644 --- a/guestbook-datomic/src/cljs/guestbook_datomic/events.cljs +++ b/guestbook-datomic/src/cljs/guestbook_datomic/events.cljs @@ -1,5 +1,8 @@ (ns guestbook-datomic.events - (:require [re-frame.core :refer [dispatch reg-event-db reg-sub]])) + (:require [re-frame.core :refer [dispatch reg-event-fx reg-event-db reg-sub]] + [day8.re-frame.http-fx] + [ajax.core :as ajax] + [clojure.set :refer [rename-keys]])) ;;dispatchers @@ -13,6 +16,53 @@ (fn [db [_ docs]] (assoc db :docs docs))) +(reg-event-db + :fetch-messages-success + (fn [db [_ messages]] + (assoc db :messages messages :show-twirly false))) + +(reg-event-db + :fetch-messages-failure + (fn [db [_ result]] + (assoc db :api-error result :show-twirly false))) + +(reg-event-fx + :fetch-messages + (fn [{:keys [db]} _] + {:db (assoc db :show-twirly true) + :http-xhrio {:method :get + :uri "/messages" + :timeout 8000 + :response-format (ajax/json-response-format {:keywords? true}) + :on-success [:fetch-messages-success] + :on-failure [:fetch-messages-failure]}})) + + +(reg-event-db + :send-message-success + (fn [db [_ message _response]] + (-> db + (assoc :show-twirly false) + (update :messages conj message)))) + +(reg-event-db + :send-message-failure + (fn [db [_ result]] + (assoc db :api-error result :show-twirly false))) + +(reg-event-fx + :send-message + (fn [{:keys [db]} [_ message]] + {:db (assoc db :show-twirly true) + :http-xhrio {:method :post + :uri "/message" + :params message + :timeout 8000 + :format (ajax/json-request-format) + :response-format (ajax/json-response-format {:keywords? true}) + :on-success [:send-message-success (rename-keys message {:message :message/message :name :message/name})] + :on-failure [:send-message-failure]}})) + ;;subscriptions (reg-sub @@ -21,6 +71,11 @@ (:page db))) (reg-sub - :docs + :show-twirly + (fn [db _] + (:show-twirly db))) + +(reg-sub + :messages (fn [db _] - (:docs db))) + (:messages db)))