├── .gitignore ├── Capstanfile ├── Dockerfile ├── Procfile ├── README.md ├── env ├── dev │ ├── clj │ │ ├── pwa │ │ │ ├── dev_middleware.clj │ │ │ ├── env.clj │ │ │ └── figwheel.clj │ │ └── user.clj │ ├── cljs │ │ └── pwa │ │ │ └── app.cljs │ └── resources │ │ ├── config.edn │ │ └── logback.xml ├── prod │ ├── clj │ │ └── pwa │ │ │ └── env.clj │ ├── cljs │ │ └── pwa │ │ │ └── app.cljs │ └── resources │ │ ├── config.edn │ │ └── logback.xml └── test │ └── resources │ ├── config.edn │ └── logback.xml ├── project.clj ├── resources ├── docs │ └── docs.md ├── html │ ├── error.html │ └── home.html └── public │ ├── css │ └── screen.css │ ├── favicon.png │ ├── img │ ├── icons │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png │ └── warning_clojure.png │ └── manifest.json ├── src ├── clj │ └── pwa │ │ ├── config.clj │ │ ├── core.clj │ │ ├── handler.clj │ │ ├── layout.clj │ │ ├── middleware.clj │ │ ├── middleware │ │ └── formats.clj │ │ ├── nrepl.clj │ │ └── routes │ │ └── home.clj ├── cljc │ └── pwa │ │ └── validation.cljc └── cljs │ └── pwa │ ├── ajax.cljs │ └── core.cljs └── test ├── clj └── pwa │ └── test │ └── handler.clj └── cljs └── pwa ├── core_test.cljs └── doo_runner.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | dev-config.edn 7 | test-config.edn 8 | *.jar 9 | *.class 10 | /.lein-* 11 | profiles.clj 12 | /.env 13 | .nrepl-port 14 | 15 | /node_modules 16 | /log 17 | -------------------------------------------------------------------------------- /Capstanfile: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Name of the base image. Capstan will download this automatically from 4 | # Cloudius S3 repository. 5 | # 6 | #base: cloudius/osv 7 | base: cloudius/osv-openjdk8 8 | 9 | # 10 | # The command line passed to OSv to start up the application. 11 | # 12 | cmdline: /java.so -jar /pwa/app.jar 13 | 14 | # 15 | # The command to use to build the application. 16 | # You can use any build tool/command (make/rake/lein/boot) - this runs locally on your machine 17 | # 18 | # For Leiningen, you can use: 19 | #build: lein uberjar 20 | # For Boot, you can use: 21 | #build: boot build 22 | 23 | # 24 | # List of files that are included in the generated image. 25 | # 26 | files: 27 | /pwa/app.jar: ./target/uberjar/pwa.jar 28 | 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-alpine 2 | 3 | COPY target/uberjar/pwa.jar /pwa/app.jar 4 | 5 | EXPOSE 3000 6 | 7 | CMD ["java", "-jar", "/pwa/app.jar"] 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java -cp target/uberjar/pwa.jar clojure.main -m pwa.core 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clojure pwa example 2 | 3 | generated using Luminus version "3.54" 4 | 5 | PWA is implemented with [Page-renderer][3]. 6 | 7 | ## Prerequisites 8 | 9 | You will need [Leiningen][1] 2.0 or above installed. 10 | 11 | [1]: https://github.com/technomancy/leiningen 12 | 13 | ## How to implement it from scratch 14 | 15 | Create a project 16 | 17 | lein new luminus pwa +cljs +reagent 18 | 19 | Swap `resources/public/favicon.ico` with `fovicon.png` and add a [manifest.json][2] file. Be sure you add icons specified in the `resources/public/img/icons` folder. 20 | 21 | [2]: https://github.com/Liverm0r/PWA-clojure/blob/master/resources/public/manifest.json 22 | 23 | Provide [Page-renderer][3] in project.clj dependencies: 24 | 25 | ```clojure 26 | (defproject pwa "0.1.0-SNAPSHOT" 27 | ... 28 | :dependencies [[page-renderer "0.4.4"]... ]... }}) 29 | ``` 30 | 31 | This Page-renderer library will do all the dirty work with the [service-worker][4] creation. 32 | 33 | Now we can provide a service worker in our routes. Open [src/clj/pwa/routes/home.clj][5] and add: 34 | 35 | ```clojure 36 | (ns pwa.routes.home 37 | (:require 38 | ... 39 | [page-renderer.api :as pr])) 40 | 41 | (defn service-worker [request] 42 | (pr/respond-service-worker 43 | {:script "/js/app.js" 44 | :sw-default-url "/app" 45 | :sw-add-assets 46 | ["/css/screen.css" 47 | "/img/warning_clojure.png" 48 | "/img/icons/icon-72x72.png" 49 | "/img/icons/icon-96x96.png" 50 | "/img/icons/icon-128x128.png" 51 | "/img/icons/icon-144x144.png" 52 | "/img/icons/icon-152x152.png" 53 | "/img/icons/icon-192x192.png" 54 | "/img/icons/icon-384x384.png" 55 | "/img/icons/icon-512x512.png"]})) 56 | 57 | (defn home-routes [] 58 | ["" 59 | {:middleware [middleware/wrap-csrf 60 | middleware/wrap-formats]} 61 | ["/" {:get home-page}] 62 | ["/app" {:get home-page}page] 63 | ["/service-worker.js" {:get service-worker}]]) 64 | ``` 65 | 66 | [3]: https://github.com/spacegangster/page-renderer 67 | [4]: https://developers.google.com/web/ilt/pwa/introduction-to-service-worker 68 | [5]: https://github.com/Liverm0r/PWA-clojure/blob/master/src/clj/pwa/routes/home.clj 69 | 70 | Refer specified manifest, favicon, and a screen.css in the `head` of `resources/html/home.html` 71 | 72 | ```html 73 | 74 | 75 | 76 | ``` 77 | 78 | Register you service worker in the `body` of `resources/html/home.html`: 79 | 80 | ```html 81 | 89 | ``` 90 | 91 | Make sure you `src/cljs/pwa/core.cljs` looks like [this][5], and you are ready to go. 92 | 93 | [5]: https://github.com/Liverm0r/PWA-clojure/blob/master/src/cljs/pwa/core.cljs 94 | 95 | ## Running 96 | 97 | To start a web server for the application, run: 98 | 99 | lein run 100 | lein figwheel 101 | 102 | ## License 103 | ``` 104 | Copyright 2019 Artur Dumchev 105 | 106 | Licensed under the Apache License, Version 2.0 (the "License"); 107 | you may not use this file except in compliance with the License. 108 | You may obtain a copy of the License at 109 | 110 | http://www.apache.org/licenses/LICENSE-2.0 111 | 112 | Unless required by applicable law or agreed to in writing, software 113 | distributed under the License is distributed on an "AS IS" BASIS, 114 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 115 | See the License for the specific language governing permissions and 116 | limitations under the License. 117 | ``` 118 | -------------------------------------------------------------------------------- /env/dev/clj/pwa/dev_middleware.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.dev-middleware 2 | (:require 3 | [ring.middleware.reload :refer [wrap-reload]] 4 | [selmer.middleware :refer [wrap-error-page]] 5 | [prone.middleware :refer [wrap-exceptions]])) 6 | 7 | (defn wrap-dev [handler] 8 | (-> handler 9 | wrap-reload 10 | wrap-error-page 11 | (wrap-exceptions {:app-namespaces ['pwa]}))) 12 | -------------------------------------------------------------------------------- /env/dev/clj/pwa/env.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.env 2 | (:require 3 | [selmer.parser :as parser] 4 | [clojure.tools.logging :as log] 5 | [pwa.dev-middleware :refer [wrap-dev]])) 6 | 7 | (def defaults 8 | {:init 9 | (fn [] 10 | (parser/cache-off!) 11 | (log/info "\n-=[pwa started successfully using the development profile]=-")) 12 | :stop 13 | (fn [] 14 | (log/info "\n-=[pwa has shut down successfully]=-")) 15 | :middleware wrap-dev}) 16 | -------------------------------------------------------------------------------- /env/dev/clj/pwa/figwheel.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.figwheel 2 | (:require [figwheel-sidecar.repl-api :as ra])) 3 | 4 | (defn start-fw [] 5 | (ra/start-figwheel!)) 6 | 7 | (defn stop-fw [] 8 | (ra/stop-figwheel!)) 9 | 10 | (defn cljs [] 11 | (ra/cljs-repl)) 12 | 13 | -------------------------------------------------------------------------------- /env/dev/clj/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | "Userspace functions you can run by default in your local REPL." 3 | (:require 4 | [pwa.config :refer [env]] 5 | [clojure.pprint] 6 | [clojure.spec.alpha :as s] 7 | [expound.alpha :as expound] 8 | [mount.core :as mount] 9 | [pwa.figwheel :refer [start-fw stop-fw cljs]] 10 | [pwa.core :refer [start-app]])) 11 | 12 | (alter-var-root #'s/*explain-out* (constantly expound/printer)) 13 | 14 | (add-tap (bound-fn* clojure.pprint/pprint)) 15 | 16 | (defn start 17 | "Starts application. 18 | You'll usually want to run this on startup." 19 | [] 20 | (mount/start-without #'pwa.core/repl-server)) 21 | 22 | (defn stop 23 | "Stops application." 24 | [] 25 | (mount/stop-except #'pwa.core/repl-server)) 26 | 27 | (defn restart 28 | "Restarts application." 29 | [] 30 | (stop) 31 | (start)) 32 | 33 | 34 | -------------------------------------------------------------------------------- /env/dev/cljs/pwa/app.cljs: -------------------------------------------------------------------------------- 1 | (ns^:figwheel-no-load pwa.app 2 | (:require 3 | [pwa.core :as core] 4 | [cljs.spec.alpha :as s] 5 | [expound.alpha :as expound] 6 | [devtools.core :as devtools])) 7 | 8 | (extend-protocol IPrintWithWriter 9 | js/Symbol 10 | (-pr-writer [sym writer _] 11 | (-write writer (str "\"" (.toString sym) "\"")))) 12 | 13 | (set! s/*explain-out* expound/printer) 14 | 15 | (enable-console-print!) 16 | 17 | (devtools/install!) 18 | 19 | (core/init!) 20 | -------------------------------------------------------------------------------- /env/dev/resources/config.edn: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /env/dev/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | UTF-8 9 | %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n 10 | 11 | 12 | 13 | log/pwa.log 14 | 15 | log/pwa.%d{yyyy-MM-dd}.%i.log 16 | 17 | 100MB 18 | 19 | 20 | 30 21 | 22 | 23 | UTF-8 24 | %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /env/prod/clj/pwa/env.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.env 2 | (:require [clojure.tools.logging :as log])) 3 | 4 | (def defaults 5 | {:init 6 | (fn [] 7 | (log/info "\n-=[pwa started successfully]=-")) 8 | :stop 9 | (fn [] 10 | (log/info "\n-=[pwa has shut down successfully]=-")) 11 | :middleware identity}) 12 | -------------------------------------------------------------------------------- /env/prod/cljs/pwa/app.cljs: -------------------------------------------------------------------------------- 1 | (ns pwa.app 2 | (:require [pwa.core :as core])) 3 | 4 | ;;ignore println statements in prod 5 | (set! *print-fn* (fn [& _])) 6 | 7 | (core/init!) 8 | -------------------------------------------------------------------------------- /env/prod/resources/config.edn: -------------------------------------------------------------------------------- 1 | {:prod true 2 | :port 3000} 3 | -------------------------------------------------------------------------------- /env/prod/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | log/pwa.log 6 | 7 | log/pwa.%d{yyyy-MM-dd}.%i.log 8 | 9 | 100MB 10 | 11 | 12 | 30 13 | 14 | 15 | UTF-8 16 | %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /env/test/resources/config.edn: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /env/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | UTF-8 9 | %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n 10 | 11 | 12 | 13 | log/pwa.log 14 | 15 | log/pwa.%d{yyyy-MM-dd}.%i.log 16 | 17 | 100MB 18 | 19 | 20 | 30 21 | 22 | 23 | UTF-8 24 | %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject pwa "0.1.0-SNAPSHOT" 2 | 3 | :description "FIXME: write description" 4 | :url "http://example.com/FIXME" 5 | 6 | :dependencies [[page-renderer "0.4.4"] 7 | [ch.qos.logback/logback-classic "1.2.3"] 8 | [cheshire "5.9.0"] 9 | [cljs-ajax "0.8.0"] 10 | [clojure.java-time "0.3.2"] 11 | [com.cognitect/transit-clj "0.8.319"] 12 | [cprop "0.1.14"] 13 | [expound "0.8.1"] 14 | [funcool/struct "1.4.0"] 15 | [luminus-jetty "0.1.7"] 16 | [luminus-transit "0.1.2"] 17 | [luminus/ring-ttl-session "0.3.3"] 18 | [markdown-clj "1.10.0"] 19 | [metosin/muuntaja "0.6.6"] 20 | [metosin/reitit "0.3.10"] 21 | [metosin/ring-http-response "0.9.1"] 22 | [mount "0.1.16"] 23 | [nrepl "0.6.0"] 24 | [org.clojure/clojure "1.10.1"] 25 | [org.clojure/clojurescript "1.10.597" :scope "provided"] 26 | [org.clojure/tools.cli "0.4.2"] 27 | [org.clojure/tools.logging "0.5.0"] 28 | [org.webjars.npm/bulma "0.8.0"] 29 | [org.webjars.npm/material-icons "0.3.1"] 30 | [org.webjars/webjars-locator "0.38"] 31 | [reagent "0.9.0-rc3"] 32 | [ring-webjars "0.2.0"] 33 | [ring/ring-core "1.8.0"] 34 | [ring/ring-defaults "0.3.2"] 35 | [selmer "1.12.17"]] 36 | 37 | :min-lein-version "2.0.0" 38 | 39 | :source-paths ["src/clj" "src/cljs" "src/cljc"] 40 | :test-paths ["test/clj"] 41 | :resource-paths ["resources" "target/cljsbuild"] 42 | :target-path "target/%s/" 43 | :main ^:skip-aot pwa.core 44 | 45 | :plugins [[lein-cljsbuild "1.1.7"]] 46 | :clean-targets ^{:protect false} 47 | [:target-path [:cljsbuild :builds :app :compiler :output-dir] [:cljsbuild :builds :app :compiler :output-to]] 48 | :figwheel 49 | {:http-server-root "public" 50 | :server-logfile "log/figwheel-logfile.log" 51 | :nrepl-port 7002 52 | :css-dirs ["resources/public/css"] 53 | :nrepl-middleware [cider.piggieback/wrap-cljs-repl]} 54 | 55 | 56 | :profiles 57 | {:uberjar {:omit-source true 58 | :prep-tasks ["compile" ["cljsbuild" "once" "min"]] 59 | :cljsbuild{:builds 60 | {:min 61 | {:source-paths ["src/cljc" "src/cljs" "env/prod/cljs"] 62 | :compiler 63 | {:output-dir "target/cljsbuild/public/js" 64 | :output-to "target/cljsbuild/public/js/app.js" 65 | :source-map "target/cljsbuild/public/js/app.js.map" 66 | :optimizations :advanced 67 | :pretty-print false 68 | :infer-externs true 69 | :closure-warnings 70 | {:externs-validation :off :non-standard-jsdoc :off} 71 | :externs ["react/externs/react.js"]}}}} 72 | 73 | :aot :all 74 | :uberjar-name "pwa.jar" 75 | :source-paths ["env/prod/clj"] 76 | :resource-paths ["env/prod/resources"]} 77 | 78 | :dev [:project/dev :profiles/dev] 79 | :test [:project/dev :project/test :profiles/test] 80 | 81 | :project/dev {:jvm-opts ["-Dconf=dev-config.edn"] 82 | :dependencies [[binaryage/devtools "0.9.10"] 83 | [cider/piggieback "0.4.2"] 84 | [doo "0.1.11"] 85 | [figwheel-sidecar "0.5.19"] 86 | [pjstadig/humane-test-output "0.10.0"] 87 | [prone "2019-07-08"] 88 | [ring/ring-devel "1.8.0"] 89 | [ring/ring-mock "0.4.0"]] 90 | :plugins [[com.jakemccrary/lein-test-refresh "0.24.1"] 91 | [jonase/eastwood "0.3.5"] 92 | [lein-doo "0.1.11"] 93 | [lein-figwheel "0.5.19"]] 94 | :cljsbuild{:builds 95 | {:app 96 | {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"] 97 | :figwheel {:on-jsload "pwa.core/mount-components"} 98 | :compiler 99 | {:main "pwa.app" 100 | :asset-path "/js/out" 101 | :output-to "target/cljsbuild/public/js/app.js" 102 | :output-dir "target/cljsbuild/public/js/out" 103 | :source-map true 104 | :optimizations :none 105 | :pretty-print true}}}} 106 | 107 | 108 | :doo {:build "test"} 109 | :source-paths ["env/dev/clj"] 110 | :resource-paths ["env/dev/resources"] 111 | :repl-options {:init-ns user} 112 | :injections [(require 'pjstadig.humane-test-output) 113 | (pjstadig.humane-test-output/activate!)]} 114 | :project/test {:jvm-opts ["-Dconf=test-config.edn"] 115 | :resource-paths ["env/test/resources"] 116 | :cljsbuild 117 | {:builds 118 | {:test 119 | {:source-paths ["src/cljc" "src/cljs" "test/cljs"] 120 | :compiler 121 | {:output-to "target/test.js" 122 | :main "pwa.doo-runner" 123 | :optimizations :whitespace 124 | :pretty-print true}}}} 125 | 126 | } 127 | :profiles/dev {} 128 | :profiles/test {}}) 129 | -------------------------------------------------------------------------------- /resources/docs/docs.md: -------------------------------------------------------------------------------- 1 |

Congratulations, your Luminus site is ready!

2 | 3 | This page will help guide you through the first steps of building your site. 4 | 5 | #### Why are you seeing this page? 6 | 7 | The `home-routes` handler in the `pwa.routes.home` namespace 8 | defines the route that invokes the `home-page` function whenever an HTTP 9 | request is made to the `/` URI using the `GET` method. 10 | 11 | ``` 12 | (defn home-routes [] 13 | ["" 14 | {:middleware [middleware/wrap-csrf 15 | middleware/wrap-formats]} 16 | ["/" {:get home-page}] 17 | ["/docs" {:get (fn [_] 18 | (-> (response/ok (-> "docs/docs.md" io/resource slurp)) 19 | (response/header "Content-Type" "text/plain; charset=utf-8")))}]]) 20 | ``` 21 | 22 | The `home-page` function will in turn call the `pwa.layout/render` function 23 | to render the HTML content: 24 | 25 | ``` 26 | (defn home-page [_] 27 | (layout/render "home.html")) 28 | ``` 29 | 30 | The page contains a link to the compiled ClojureScript found in the `target/cljsbuild/public` folder: 31 | 32 | ``` 33 | {% script "/js/app.js" %} 34 | ``` 35 | 36 | The rest of this page is rendered by ClojureScript found in the `src/cljs/pwa/core.cljs` file. 37 | 38 | 39 | 40 | #### Organizing the routes 41 | 42 | The routes are aggregated and wrapped with middleware in the `pwa.handler` namespace: 43 | 44 | ``` 45 | (mount/defstate app 46 | :start 47 | (middleware/wrap-base 48 | (ring/ring-handler 49 | (ring/router 50 | [(home-routes)]) 51 | (ring/routes 52 | (ring/create-resource-handler 53 | {:path "/"}) 54 | (wrap-content-type 55 | (wrap-webjars (constantly nil))) 56 | (ring/create-default-handler 57 | {:not-found 58 | (constantly (error-page {:status 404, :title "404 - Page not found"})) 59 | :method-not-allowed 60 | (constantly (error-page {:status 405, :title "405 - Not allowed"})) 61 | :not-acceptable 62 | (constantly (error-page {:status 406, :title "406 - Not acceptable"}))}))))) 63 | ``` 64 | 65 | The `app` definition groups all the routes in the application into a single handler. 66 | A default route group is added to handle the `404`, `405`, and `406` errors. 67 | 68 | learn more about routing » 69 | 70 | #### Managing your middleware 71 | 72 | Request middleware functions are located under the `pwa.middleware` namespace. 73 | 74 | This namespace is reserved for any custom middleware for the application. Some default middleware is 75 | already defined here. The middleware is assembled in the `wrap-base` function. 76 | 77 | Middleware used for development is placed in the `pwa.dev-middleware` namespace found in 78 | the `env/dev/clj/` source path. 79 | 80 | learn more about middleware » 81 | 82 | 83 | 84 | 85 | #### Need some help? 86 | 87 | Visit the [official documentation](http://www.luminusweb.net/docs) for examples 88 | 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. 89 | -------------------------------------------------------------------------------- /resources/html/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Something Bad Happened 5 | 6 | 7 | {% style "/assets/bulma/css/bulma.min.css" %} 8 | 34 | 35 | 36 |
37 |
38 |

Error: {{status}}

39 |
40 | {% if title %} 41 |

{{title}}

42 | {% endif %} 43 | {% if message %} 44 |

{{message}}

45 | {% endif %} 46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /resources/html/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Welcome to pwa 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |

Welcome to pwa

18 |

If you're seeing this message, that means you haven't yet compiled your ClojureScript!

19 |

Please run lein figwheel to start the ClojureScript compiler and reload the page.

20 |

For better ClojureScript development experience in Chrome follow these steps:

21 |
    22 |
  • Open DevTools 23 |
  • Go to Settings ("three dots" icon in the upper right corner of DevTools > Menu > Settings F1 > General > Console) 24 |
  • Check-in "Enable custom formatters" 25 |
  • Close DevTools 26 |
  • Open DevTools 27 |
28 |

See ClojureScript documentation for further details.

29 |
30 |
31 |
32 |
33 | 34 | 35 | {% style "/assets/bulma/css/bulma.min.css" %} 36 | {% style "/assets/material-icons/css/material-icons.min.css" %} 37 | {% style "/css/screen.css" %} 38 | 39 | 42 | 43 | 51 | 52 | {% script "/js/app.js" %} 53 | 54 | 55 | -------------------------------------------------------------------------------- /resources/public/css/screen.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 4 | } 5 | .vertical-container { 6 | height: 300px; 7 | display: -webkit-flex; 8 | display: flex; 9 | -webkit-align-items: center; 10 | align-items: center; 11 | -webkit-justify-content: center; 12 | justify-content: center; 13 | } 14 | @font-face { 15 | font-family: 'Material Icons'; 16 | font-style: normal; 17 | font-weight: 400; 18 | src: url(/assets/material-icons/iconfont/MaterialIcons-Regular.eot); /* For IE6-8 */ 19 | src: local('Material Icons'), 20 | local('MaterialIcons-Regular'), 21 | url(/assets/material-icons/iconfont/MaterialIcons-Regular.woff2) format('woff2'), 22 | url(/assets/material-icons/iconfont/MaterialIcons-Regular.woff) format('woff'), 23 | url(/assets/material-icons/iconfont/MaterialIcons-Regular.ttf) format('truetype'); 24 | } 25 | .material-icons { 26 | font-family: 'Material Icons'; 27 | font-weight: normal; 28 | font-style: normal; 29 | font-size: 24px; /* Preferred icon size */ 30 | display: inline-block; 31 | line-height: 1; 32 | text-transform: none; 33 | letter-spacing: normal; 34 | word-wrap: normal; 35 | white-space: nowrap; 36 | direction: ltr; 37 | /* Support for all WebKit browsers. */ 38 | -webkit-font-smoothing: antialiased; 39 | /* Support for Safari and Chrome. */ 40 | text-rendering: optimizeLegibility; 41 | /* Support for Firefox. */ 42 | -moz-osx-font-smoothing: grayscale; 43 | } 44 | 45 | -------------------------------------------------------------------------------- /resources/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D00mch/PWA-clojure/39ab4d3690c7a8ddbdd8095d65a782961f92c183/resources/public/favicon.png -------------------------------------------------------------------------------- /resources/public/img/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D00mch/PWA-clojure/39ab4d3690c7a8ddbdd8095d65a782961f92c183/resources/public/img/icons/icon-128x128.png -------------------------------------------------------------------------------- /resources/public/img/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D00mch/PWA-clojure/39ab4d3690c7a8ddbdd8095d65a782961f92c183/resources/public/img/icons/icon-144x144.png -------------------------------------------------------------------------------- /resources/public/img/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D00mch/PWA-clojure/39ab4d3690c7a8ddbdd8095d65a782961f92c183/resources/public/img/icons/icon-152x152.png -------------------------------------------------------------------------------- /resources/public/img/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D00mch/PWA-clojure/39ab4d3690c7a8ddbdd8095d65a782961f92c183/resources/public/img/icons/icon-192x192.png -------------------------------------------------------------------------------- /resources/public/img/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D00mch/PWA-clojure/39ab4d3690c7a8ddbdd8095d65a782961f92c183/resources/public/img/icons/icon-384x384.png -------------------------------------------------------------------------------- /resources/public/img/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D00mch/PWA-clojure/39ab4d3690c7a8ddbdd8095d65a782961f92c183/resources/public/img/icons/icon-512x512.png -------------------------------------------------------------------------------- /resources/public/img/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D00mch/PWA-clojure/39ab4d3690c7a8ddbdd8095d65a782961f92c183/resources/public/img/icons/icon-72x72.png -------------------------------------------------------------------------------- /resources/public/img/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D00mch/PWA-clojure/39ab4d3690c7a8ddbdd8095d65a782961f92c183/resources/public/img/icons/icon-96x96.png -------------------------------------------------------------------------------- /resources/public/img/warning_clojure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D00mch/PWA-clojure/39ab4d3690c7a8ddbdd8095d65a782961f92c183/resources/public/img/warning_clojure.png -------------------------------------------------------------------------------- /resources/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PWA example", 3 | "short_name": "PWA", 4 | "theme_color": "#2196f3", 5 | "background_color": "#2196f3", 6 | "display": "standalone", 7 | "scope": "/app", 8 | "start_url": "/app", 9 | "icons": [ 10 | { 11 | "src": "img/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "img/icons/icon-96x96.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "img/icons/icon-128x128.png", 22 | "sizes": "128x128", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "img/icons/icon-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "img/icons/icon-152x152.png", 32 | "sizes": "152x152", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "img/icons/icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png" 39 | }, 40 | { 41 | "src": "img/icons/icon-384x384.png", 42 | "sizes": "384x384", 43 | "type": "image/png" 44 | }, 45 | { 46 | "src": "img/icons/icon-512x512.png", 47 | "sizes": "512x512", 48 | "type": "image/png" 49 | } 50 | ], 51 | "splash_pages": null 52 | } 53 | -------------------------------------------------------------------------------- /src/clj/pwa/config.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.config 2 | (:require 3 | [cprop.core :refer [load-config]] 4 | [cprop.source :as source] 5 | [mount.core :refer [args defstate]])) 6 | 7 | (defstate env 8 | :start 9 | (load-config 10 | :merge 11 | [(args) 12 | (source/from-system-props) 13 | (source/from-env)])) 14 | -------------------------------------------------------------------------------- /src/clj/pwa/core.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.core 2 | (:require 3 | [pwa.handler :as handler] 4 | [pwa.nrepl :as nrepl] 5 | [luminus.http-server :as http] 6 | [pwa.config :refer [env]] 7 | [clojure.tools.cli :refer [parse-opts]] 8 | [clojure.tools.logging :as log] 9 | [mount.core :as mount]) 10 | (:gen-class)) 11 | 12 | ;; log uncaught exceptions in threads 13 | (Thread/setDefaultUncaughtExceptionHandler 14 | (reify Thread$UncaughtExceptionHandler 15 | (uncaughtException [_ thread ex] 16 | (log/error {:what :uncaught-exception 17 | :exception ex 18 | :where (str "Uncaught exception on" (.getName thread))})))) 19 | 20 | (def cli-options 21 | [["-p" "--port PORT" "Port number" 22 | :parse-fn #(Integer/parseInt %)]]) 23 | 24 | (mount/defstate ^{:on-reload :noop} http-server 25 | :start 26 | (http/start 27 | (-> env 28 | (assoc :handler (handler/app)) 29 | (update :io-threads #(or % (* 2 (.availableProcessors (Runtime/getRuntime))))) 30 | (update :port #(or (-> env :options :port) %)))) 31 | :stop 32 | (http/stop http-server)) 33 | 34 | (mount/defstate ^{:on-reload :noop} repl-server 35 | :start 36 | (when (env :nrepl-port) 37 | (nrepl/start {:bind (env :nrepl-bind) 38 | :port (env :nrepl-port)})) 39 | :stop 40 | (when repl-server 41 | (nrepl/stop repl-server))) 42 | 43 | 44 | (defn stop-app [] 45 | (doseq [component (:stopped (mount/stop))] 46 | (log/info component "stopped")) 47 | (shutdown-agents)) 48 | 49 | (defn start-app [args] 50 | (doseq [component (-> args 51 | (parse-opts cli-options) 52 | mount/start-with-args 53 | :started)] 54 | (log/info component "started")) 55 | (.addShutdownHook (Runtime/getRuntime) (Thread. stop-app))) 56 | 57 | (defn -main [& args] 58 | (start-app args)) 59 | -------------------------------------------------------------------------------- /src/clj/pwa/handler.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.handler 2 | (:require 3 | [pwa.middleware :as middleware] 4 | [pwa.layout :refer [error-page]] 5 | [pwa.routes.home :refer [home-routes]] 6 | [reitit.ring :as ring] 7 | [ring.middleware.content-type :refer [wrap-content-type]] 8 | [ring.middleware.webjars :refer [wrap-webjars]] 9 | [pwa.env :refer [defaults]] 10 | [mount.core :as mount])) 11 | 12 | (mount/defstate init-app 13 | :start ((or (:init defaults) (fn []))) 14 | :stop ((or (:stop defaults) (fn [])))) 15 | 16 | (mount/defstate app-routes 17 | :start 18 | (ring/ring-handler 19 | (ring/router 20 | [(home-routes)]) 21 | (ring/routes 22 | (ring/create-resource-handler 23 | {:path "/"}) 24 | (wrap-content-type 25 | (wrap-webjars (constantly nil))) 26 | (ring/create-default-handler 27 | {:not-found 28 | (constantly (error-page {:status 404, :title "404 - Page not found"})) 29 | :method-not-allowed 30 | (constantly (error-page {:status 405, :title "405 - Not allowed"})) 31 | :not-acceptable 32 | (constantly (error-page {:status 406, :title "406 - Not acceptable"}))})))) 33 | 34 | (defn app [] 35 | (middleware/wrap-base #'app-routes)) 36 | -------------------------------------------------------------------------------- /src/clj/pwa/layout.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.layout 2 | (:require 3 | [clojure.java.io] 4 | [selmer.parser :as parser] 5 | [selmer.filters :as filters] 6 | [markdown.core :refer [md-to-html-string]] 7 | [ring.util.http-response :refer [content-type ok]] 8 | [ring.util.anti-forgery :refer [anti-forgery-field]] 9 | [ring.middleware.anti-forgery :refer [*anti-forgery-token*]] 10 | [ring.util.response])) 11 | 12 | (parser/set-resource-path! (clojure.java.io/resource "html")) 13 | (parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field))) 14 | (filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)])) 15 | 16 | (defn render 17 | "renders the HTML template located relative to resources/html" 18 | [request template & [params]] 19 | (content-type 20 | (ok 21 | (parser/render-file 22 | template 23 | (assoc params 24 | :page template 25 | :csrf-token *anti-forgery-token*))) 26 | "text/html; charset=utf-8")) 27 | 28 | (defn error-page 29 | "error-details should be a map containing the following keys: 30 | :status - error status 31 | :title - error title (optional) 32 | :message - detailed error message (optional) 33 | 34 | returns a response map with the error page as the body 35 | and the status specified by the status key" 36 | [error-details] 37 | {:status (:status error-details) 38 | :headers {"Content-Type" "text/html; charset=utf-8"} 39 | :body (parser/render-file "error.html" error-details)}) 40 | -------------------------------------------------------------------------------- /src/clj/pwa/middleware.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.middleware 2 | (:require 3 | [pwa.env :refer [defaults]] 4 | [cheshire.generate :as cheshire] 5 | [cognitect.transit :as transit] 6 | [clojure.tools.logging :as log] 7 | [pwa.layout :refer [error-page]] 8 | [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] 9 | [pwa.middleware.formats :as formats] 10 | [muuntaja.middleware :refer [wrap-format wrap-params]] 11 | [pwa.config :refer [env]] 12 | [ring-ttl-session.core :refer [ttl-memory-store]] 13 | [ring.middleware.defaults :refer [site-defaults wrap-defaults]]) 14 | 15 | ) 16 | 17 | (defn wrap-internal-error [handler] 18 | (fn [req] 19 | (try 20 | (handler req) 21 | (catch Throwable t 22 | (log/error t (.getMessage t)) 23 | (error-page {:status 500 24 | :title "Something very bad has happened!" 25 | :message "We've dispatched a team of highly trained gnomes to take care of the problem."}))))) 26 | 27 | (defn wrap-csrf [handler] 28 | (wrap-anti-forgery 29 | handler 30 | {:error-response 31 | (error-page 32 | {:status 403 33 | :title "Invalid anti-forgery token"})})) 34 | 35 | 36 | (defn wrap-formats [handler] 37 | (let [wrapped (-> handler wrap-params (wrap-format formats/instance))] 38 | (fn [request] 39 | ;; disable wrap-formats for websockets 40 | ;; since they're not compatible with this middleware 41 | ((if (:websocket? request) handler wrapped) request)))) 42 | 43 | (defn wrap-base [handler] 44 | (-> ((:middleware defaults) handler) 45 | (wrap-defaults 46 | (-> site-defaults 47 | (assoc-in [:security :anti-forgery] false) 48 | (assoc-in [:session :store] (ttl-memory-store (* 60 30))))) 49 | wrap-internal-error)) 50 | -------------------------------------------------------------------------------- /src/clj/pwa/middleware/formats.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.middleware.formats 2 | (:require 3 | [cognitect.transit :as transit] 4 | [luminus-transit.time :as time] 5 | [muuntaja.core :as m])) 6 | 7 | (def instance 8 | (m/create 9 | (-> m/default-options 10 | (update-in 11 | [:formats "application/transit+json" :decoder-opts] 12 | (partial merge time/time-deserialization-handlers)) 13 | (update-in 14 | [:formats "application/transit+json" :encoder-opts] 15 | (partial merge time/time-serialization-handlers))))) 16 | -------------------------------------------------------------------------------- /src/clj/pwa/nrepl.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.nrepl 2 | (:require 3 | [nrepl.server :as nrepl] 4 | [clojure.tools.logging :as log])) 5 | 6 | (defn start 7 | "Start a network repl for debugging on specified port followed by 8 | an optional parameters map. The :bind, :transport-fn, :handler, 9 | :ack-port and :greeting-fn will be forwarded to 10 | clojure.tools.nrepl.server/start-server as they are." 11 | [{:keys [port bind transport-fn handler ack-port greeting-fn]}] 12 | (try 13 | (log/info "starting nREPL server on port" port) 14 | (nrepl/start-server :port port 15 | :bind bind 16 | :transport-fn transport-fn 17 | :handler handler 18 | :ack-port ack-port 19 | :greeting-fn greeting-fn) 20 | 21 | (catch Throwable t 22 | (log/error t "failed to start nREPL") 23 | (throw t)))) 24 | 25 | (defn stop [server] 26 | (nrepl/stop-server server) 27 | (log/info "nREPL server stopped")) 28 | -------------------------------------------------------------------------------- /src/clj/pwa/routes/home.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.routes.home 2 | (:require 3 | [pwa.layout :as layout] 4 | [clojure.java.io :as io] 5 | [pwa.middleware :as middleware] 6 | [ring.util.response] 7 | [page-renderer.api :as pr] 8 | [ring.util.http-response :as response])) 9 | 10 | (defn service-worker [request] 11 | (pr/respond-service-worker 12 | {:script "/js/app.js" 13 | :sw-default-url "/app" 14 | :sw-add-assets 15 | ["/css/screen.css" 16 | "/img/warning_clojure.png" 17 | "/img/icons/icon-72x72.png" 18 | "/img/icons/icon-96x96.png" 19 | "/img/icons/icon-128x128.png" 20 | "/img/icons/icon-144x144.png" 21 | "/img/icons/icon-152x152.png" 22 | "/img/icons/icon-192x192.png" 23 | "/img/icons/icon-384x384.png" 24 | "/img/icons/icon-512x512.png"]})) 25 | 26 | (defn home-page [request] 27 | (layout/render request "home.html")) 28 | 29 | (defn home-routes [] 30 | ["" 31 | {:middleware [middleware/wrap-csrf 32 | middleware/wrap-formats]} 33 | ["/" {:get home-page}] 34 | ["/service-worker.js" {:get service-worker}] 35 | ["/app" {:get home-page}]]) 36 | 37 | -------------------------------------------------------------------------------- /src/cljc/pwa/validation.cljc: -------------------------------------------------------------------------------- 1 | (ns pwa.validation 2 | (:require [struct.core :as st])) 3 | -------------------------------------------------------------------------------- /src/cljs/pwa/ajax.cljs: -------------------------------------------------------------------------------- 1 | (ns pwa.ajax 2 | (:require 3 | [ajax.core :as ajax] 4 | [luminus-transit.time :as time] 5 | [cognitect.transit :as transit])) 6 | 7 | (defn local-uri? [{:keys [uri]}] 8 | (not (re-find #"^\w+?://" uri))) 9 | 10 | (defn default-headers [request] 11 | (if (local-uri? request) 12 | (-> request 13 | (update :headers #(merge {"x-csrf-token" js/csrfToken} %))) 14 | request)) 15 | 16 | ;; injects transit serialization config into request options 17 | (defn as-transit [opts] 18 | (merge {:raw false 19 | :format :transit 20 | :response-format :transit 21 | :reader (transit/reader :json time/time-deserialization-handlers) 22 | :writer (transit/writer :json time/time-serialization-handlers)} 23 | opts)) 24 | 25 | (defn load-interceptors! [] 26 | (swap! ajax/default-interceptors 27 | conj 28 | (ajax/to-interceptor {:name "default headers" 29 | :request default-headers}))) 30 | -------------------------------------------------------------------------------- /src/cljs/pwa/core.cljs: -------------------------------------------------------------------------------- 1 | (ns pwa.core 2 | (:require 3 | [reagent.core :as r] 4 | [goog.events :as events] 5 | [goog.history.EventType :as HistoryEventType] 6 | [markdown.core :refer [md->html]] 7 | [pwa.ajax :as ajax] 8 | [ajax.core :refer [GET POST]] 9 | [reitit.core :as reitit] 10 | [clojure.string :as string]) 11 | (:import goog.History)) 12 | 13 | (defonce session (r/atom {:page :home})) 14 | 15 | (defn nav-link [uri title page] 16 | [:a.navbar-item 17 | {:href uri 18 | :class (when (= page (:page @session)) "is-active")} 19 | title]) 20 | 21 | (defn navbar [] 22 | (r/with-let [expanded? (r/atom false)] 23 | [:nav.navbar.is-info>div.container 24 | [:div.navbar-brand 25 | [:a.navbar-item {:href "/" :style {:font-weight :bold}} "pwa"] 26 | [:span.navbar-burger.burger 27 | {:data-target :nav-menu 28 | :on-click #(swap! expanded? not) 29 | :class (when @expanded? :is-active)} 30 | [:span][:span][:span]]] 31 | [:div#nav-menu.navbar-menu 32 | {:class (when @expanded? :is-active)} 33 | [:div.navbar-start 34 | [nav-link "#/" "Home" :home] 35 | [nav-link "#/about" "About" :about]]]])) 36 | 37 | (defn about-page [] 38 | [:section.section>div.container>div.content 39 | [:img {:src "/img/warning_clojure.png"}]]) 40 | 41 | (defn home-page [] 42 | [:section.section>div.container>div.content 43 | [:div [:text "some text"]]]) 44 | 45 | (def pages 46 | {:home #'home-page 47 | :about #'about-page}) 48 | 49 | (defn page [] 50 | [(pages (:page @session))]) 51 | 52 | ;; ------------------------- 53 | ;; Routes 54 | 55 | (def router 56 | (reitit/router 57 | [["/" :home] 58 | ["/about" :about]])) 59 | 60 | (defn match-route [uri] 61 | (->> (or (not-empty (string/replace uri #"^.*#" "")) "/") 62 | (reitit/match-by-path router) 63 | :data 64 | :name)) 65 | ;; ------------------------- 66 | ;; History 67 | ;; must be called after routes have been defined 68 | (defn hook-browser-navigation! [] 69 | (doto (History.) 70 | (events/listen 71 | HistoryEventType/NAVIGATE 72 | (fn [event] 73 | (swap! session assoc :page (match-route (.-token event))))) 74 | (.setEnabled true))) 75 | 76 | ;; ------------------------- 77 | ;; Initialize app 78 | 79 | (defn mount-components [] 80 | (r/render [#'navbar] (.getElementById js/document "navbar")) 81 | (r/render [#'page] (.getElementById js/document "app"))) 82 | 83 | (defn init! [] 84 | (ajax/load-interceptors!) 85 | (hook-browser-navigation!) 86 | (mount-components)) 87 | -------------------------------------------------------------------------------- /test/clj/pwa/test/handler.clj: -------------------------------------------------------------------------------- 1 | (ns pwa.test.handler 2 | (:require 3 | [clojure.test :refer :all] 4 | [ring.mock.request :refer :all] 5 | [pwa.handler :refer :all] 6 | [pwa.middleware.formats :as formats] 7 | [muuntaja.core :as m] 8 | [mount.core :as mount])) 9 | 10 | (defn parse-json [body] 11 | (m/decode formats/instance "application/json" body)) 12 | 13 | (use-fixtures 14 | :once 15 | (fn [f] 16 | (mount/start #'pwa.config/env 17 | #'pwa.handler/app-routes) 18 | (f))) 19 | 20 | (deftest test-app 21 | (testing "main route" 22 | (let [response ((app) (request :get "/"))] 23 | (is (= 200 (:status response))))) 24 | 25 | (testing "not-found route" 26 | (let [response ((app) (request :get "/invalid"))] 27 | (is (= 404 (:status response)))))) 28 | -------------------------------------------------------------------------------- /test/cljs/pwa/core_test.cljs: -------------------------------------------------------------------------------- 1 | (ns pwa.core-test 2 | (:require [cljs.test :refer-macros [is are deftest testing use-fixtures]] 3 | [pjstadig.humane-test-output] 4 | [reagent.core :as reagent :refer [atom]] 5 | [pwa.core :as rc])) 6 | 7 | (deftest test-home 8 | (is (= true true))) 9 | 10 | -------------------------------------------------------------------------------- /test/cljs/pwa/doo_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns pwa.doo-runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [pwa.core-test])) 4 | 5 | (doo-tests 'pwa.core-test) 6 | 7 | --------------------------------------------------------------------------------