├── .gitignore
├── Justfile
├── README.md
├── build.clj
├── deps.edn
├── dev-src
└── user.clj
├── package.json
├── postcss.config.js
├── resources
├── config.edn
├── data
│ ├── pg-1-products.csv
│ ├── pg-2-products.csv
│ └── product-groups.csv
├── logback.xml
└── public
│ ├── favicon.ico
│ ├── index.html
│ └── info.html
├── scripts
├── get-ping.sh
├── info.sh
├── login.sh
├── post-ping.sh
├── product-2-49.sh
├── product-groups.sh
└── products-1.sh
├── shadow-cljs.edn
├── src
├── clj
│ └── backend
│ │ ├── core.clj
│ │ ├── db
│ │ ├── domain.clj
│ │ └── users.clj
│ │ └── webserver.clj
├── cljs
│ └── frontend
│ │ ├── header.cljs
│ │ ├── http.cljs
│ │ ├── main.cljs
│ │ ├── routes
│ │ ├── index.cljs
│ │ ├── login.cljs
│ │ ├── product.cljs
│ │ ├── product_groups.cljs
│ │ └── products.cljs
│ │ ├── state.cljs
│ │ └── util.cljs
└── css
│ └── app.css
├── tailwind.config.js
└── test
└── clj
└── backend
├── db
├── domain_test.clj
└── users_test.clj
├── test_config.clj
└── webserver_test.clj
/.gitignore:
--------------------------------------------------------------------------------
1 | # Shadow
2 | dev-resources/
3 | node_modules/
4 | target/
5 | yarn.lock
6 | package-lock.json
7 | .shadow-cljs/
8 | prod-resources/
9 | .clj-kondo/
10 | .cpcache/
11 | .lsp/
12 | tmp/
13 | personal/
14 | .calva/
15 | .nrepl-port
16 | logs/
17 | scratch/
18 |
--------------------------------------------------------------------------------
/Justfile:
--------------------------------------------------------------------------------
1 | @list:
2 | just --list
3 |
4 | # NOTE: The backend repls scripts are not needed if you use Calva connection "backend + frontend"
5 | # Just start the 'just frontend', since Calva connects to the frontend
6 | # and uses the backend repl via shadow-cljs.
7 | # NOTE: With Calva, see later: frontend
8 | @backend-calva:
9 | clj -M:dev:test:common:backend:calva:kari
10 | @backend-calva-nrepl:
11 | clj -M:dev:test:common:backend:calva:kari -m nrepl.cmdline
12 |
13 | @backend-run-tests:
14 | clj -M:dev:test:common:backend -m kaocha.runner
15 |
16 | # Init node packages.
17 | @init:
18 | mkdir -p target
19 | mkdir -p classes
20 | rm -rf node_modules
21 | npm install
22 |
23 | # Start frontend auto-compilation, and also needed for Calva to connect to the backend.
24 | # Then in Calva give command: `Calva: Connect to a running REPL Server in the Project`.
25 | # With Calva choose this => Then in Calva choose: Connect to a running REPL... => `backend + frontend`.
26 | @frontend:
27 | rm -rf dev-resources
28 | mkdir -p dev-resources/public/js
29 | npx tailwindcss -i ./src/css/app.css -o ./dev-resources/public/index.css
30 | npm run dev
31 |
32 | @tailwind:
33 | npx tailwindcss -i ./src/css/app.css -o ./dev-resources/public/index.css --watch
34 |
35 | @build-uber:
36 | echo "***** Building frontend *****"
37 | rm -rf prod-resources
38 | mkdir -p prod-resources/public/js
39 | npx tailwindcss -i ./src/css/app.css -o ./prod-resources/public/index.css
40 | npx shadow-cljs release app
41 | echo "***** Building backend *****"
42 | clj -T:build uber
43 |
44 | @run-uber:
45 | PROFILE=prod java -jar target/karimarttila/webstore-standalone.jar
46 |
47 |
48 | # Update dependencies.
49 | @outdated:
50 | # Backend
51 | # See: https://github.com/liquidz/antq#clojure-cli-tools-11111139-or-later
52 | clj -Tantq outdated :upgrade true
53 | # Frontend
54 | # Install: npm i -g npm-check-updates
55 | rm -rf node_modules
56 | ncu -u
57 | npm install
58 |
59 | # Clean .cpcache and .shadow-cljs directories, run npm install
60 | @clean:
61 | rm -rf .cpcache/*
62 | rm -rf .shadow-cljs/*
63 | rm -rf target/*
64 | rm -rf dev-resources/*
65 | rm -rf prod-resources/*
66 | rm -rf out/*
67 | npm install
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Clojure (backend) / Clojurescript-React (frontend) demonstration app
2 |
3 | ## Introduction
4 |
5 | I previously created a Javascript-Node (backend) / Typescript-React (frontend) demonstration for my personal learning purposes: [js-node-ts-react](https://github.com/karimarttila/js-node-ts-react). I promised to keep a Clojure lecture in my new corporation, and for this lecture, I thought that it might be interesting for the audience to provide two exact same full-stack applications: one written using the Javascript ecosystem, and one using Clojure. This way my colleagues can, later on, compare these two solutions and make it easier to start learning Clojure when you can compare various backend and frontend solutions to a more familiar Javascript/Typescript example. I have actually created several versions of this webstore example using several languages.
6 |
7 | I previously created the same demonstration ([re-frame-demo](https://github.com/karimarttila/clojure/tree/master/webstore-demo/re-frame-demo)), but in this new repo I modernized that solution and made the solution to be as similar as possible to the Javascript/Typescript example.
8 |
9 | There is also a blog post that compares these two solutions: [Comparing Clojure and Javascript](https://www.karimarttila.fi/clojure/2023/02/TODO/comparing-clojure-and-javascript.html). TODO: Update the link when the blog post is published.
10 |
11 |
12 | ## Prerequisites
13 |
14 | - Install [just](https://github.com/casey/just) command line runner.
15 | - See instructions for configuring Calva in [Configuring VSCode/Calva for Clojure programming - Part 3](https://www.karimarttila.fi/clojure/2022/10/18/clojure-calva-part3.html).
16 |
17 | ## Quick Start Guide for Calva Users
18 |
19 | These instructions apply to Calva users. If you are not using Calva, you can still follow the instructions, but you will need to start the backend REPL also.
20 |
21 | Both the backend and the frontend use the same port 7171.
22 |
23 | Using Calva you just need to start the frontend REPL: `just frontend`.
24 |
25 | Then in Calva give command: `Calva: Connect to a running REPL Server in the Project`. Then: `backend + frontend` (see: [Configuring VSCode/Calva for Clojure programming - Part 3](https://www.karimarttila.fi/clojure/2022/10/18/clojure-calva-part3.html)).
26 |
27 | Now you have the REPL in Calva, the output window is the same for clj and cljs repls.
28 |
29 | Once you have both frontend and backend REPLs running, and you have done the Integrant reset (see in file `user.clj`: `(reset)` ), you can see the frontend in browser at `http://localhost:7171`. The backend is also running at the same port, see examples in the `scripts` directory.
30 |
31 | Start Tailwind CSS processing by: `just tailwind`.
32 |
--------------------------------------------------------------------------------
/build.clj:
--------------------------------------------------------------------------------
1 | (ns build
2 | (:refer-clojure :exclude [test])
3 | (:require [clojure.tools.build.api :as b]))
4 |
5 | (def lib 'karimarttila/webstore)
6 | (def main 'backend.core)
7 | (def class-dir "target/classes")
8 |
9 | (defn- uber-opts [opts]
10 | (assoc opts
11 | :lib lib
12 | :main main
13 | :uber-file (format "target/%s-standalone.jar" lib)
14 | :basis (b/create-basis {:project "deps.edn"
15 | :aliases [:common :backend :frontend]
16 | }
17 | )
18 | :class-dir class-dir
19 | :src-dirs ["src/clj" "src/cljc"]
20 | :ns-compile [main]))
21 |
22 | (defn uber [opts]
23 | (println "Cleaning...")
24 | (b/delete {:path "target"})
25 | (let [opts (uber-opts opts)]
26 | (println "Copying files...")
27 | (b/copy-dir {:src-dirs ["resources" "prod-resources" "src/clj" "src/cljc"]
28 | :target-dir class-dir})
29 | (println "Compiling files...")
30 | (b/compile-clj opts)
31 | (println "Creating uberjar...")
32 | (b/uber opts)))
33 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["resources"]
2 | :aliases {:dev {:extra-paths ["dev-resources" "dev-src"]
3 | :extra-deps {org.clojure/clojure {:mvn/version "1.11.1"}
4 | thheller/shadow-cljs {:mvn/version "2.22.9"}
5 | clj-kondo/clj-kondo {:mvn/version "2023.03.17"}
6 | day8.re-frame/re-frame-10x {:mvn/version "1.5.0"}
7 | metosin/reagent-dev-tools {:mvn/version "1.0.0"}}}
8 | :test {:extra-paths ["test/clj"]
9 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.80.1274"}
10 | lambdaisland/kaocha-cljs {:mvn/version "1.4.130"}}}
11 | :common {:extra-paths ["src/cljc"]
12 | :extra-deps {metosin/reitit {:mvn/version "0.6.0"}
13 | aero/aero {:mvn/version "1.1.6"}
14 | metosin/potpuri {:mvn/version "0.5.3"}
15 | integrant/repl {:mvn/version "0.3.2"}}}
16 | :backend {:extra-paths ["src/clj"]
17 | :extra-deps {metosin/ring-http-response {:mvn/version "0.9.3"}
18 | integrant/integrant {:mvn/version "0.8.0"}
19 | clj-time/clj-time {:mvn/version "0.15.2"}
20 | org.clojure/data.codec {:mvn/version "0.1.1"}
21 | org.clojure/data.json {:mvn/version "2.4.0"}
22 | org.clojure/data.csv {:mvn/version "1.0.1"}
23 | org.clojure/tools.logging {:mvn/version "1.2.4"}
24 | ch.qos.logback/logback-classic {:mvn/version "1.4.6"}
25 | ring/ring-core {:mvn/version "1.9.6"}
26 | ring/ring-jetty-adapter {:mvn/version "1.9.6"}
27 | ring/ring-json {:mvn/version "0.5.1"}
28 | ring/ring-defaults {:mvn/version "0.3.4"}
29 | buddy/buddy-sign {:mvn/version "3.4.333"}
30 | clj-http/clj-http {:mvn/version "3.12.3"}
31 | nrepl/nrepl {:mvn/version "1.0.0"}}}
32 |
33 | :frontend {:extra-paths ["src/cljs"]
34 | :extra-deps {org.clojure/clojurescript {:mvn/version "1.11.60"}
35 | fipp/fipp {:mvn/version "0.6.26"
36 | :exclusions [org.clojure/core.rrb-vector]}
37 | reagent/reagent {:mvn/version "1.2.0"
38 | :exclusions [cljsjs.react-dom/cljsjs.react-dom]}
39 | re-frame/re-frame {:mvn/version "1.3.0"}
40 | day8.re-frame/http-fx {:mvn/version "0.2.4"}
41 | binaryage/devtools {:mvn/version "1.0.6"}}}
42 | :css {:extra-deps {deraen/sass4clj {:mvn/version "0.5.5"}
43 | ch.qos.logback/logback-classic {:mvn/version "1.4.6"}}
44 | :main-opts ["-m" "sass4clj.main"
45 | "--source-paths" "src/cljs/simplefrontend"
46 | "--inputs" "main.scss"
47 | "--target-path" "target/shadow/dev/resources/public/css"]}
48 | :shadow-cljs {:main-opts ["-m" "shadow.cljs.devtools.cli"]}
49 |
50 | ;; Test runner in console.
51 | :test-runner {:extra-paths ["test"]
52 | :extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git"
53 | :sha "7284cda41fb9edc0f3bc6b6185cfb7138fc8a023"}}
54 | :main-opts ["-m" "cognitect.test-runner"]}
55 |
56 |
57 | :clj-kondo {:extra-deps {clj-kondo/clj-kondo {:mvn/version "2023.03.17"}}
58 | :main-opts ["-m" "clj-kondo.main"]}
59 | ;; Calva-specific
60 | :calva {:extra-deps {cider/cider-nrepl {:mvn/version,"0.30.0"}}}
61 |
62 | :build {:deps {io.github.clojure/tools.build {:mvn/version "0.9.4"}}
63 | :ns-default build}
64 | ;; TODO remove later
65 | #_#_:uberjar {:extra-deps {seancorfield/depstar {:mvn/version "2.0.216"}}
66 | :extra-paths ["prod-resources"]
67 | :main-opts ["-m" "hf.depstar.uberjar" "target/simpleserver.jar"]}
68 | }}
69 |
70 |
--------------------------------------------------------------------------------
/dev-src/user.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require [integrant.repl :refer [reset]]
3 | ;[integrant.repl :refer [clear go halt prep init reset reset-all]]
4 | [integrant.repl.state :as state]
5 | [backend.core :as core]
6 | ))
7 |
8 | (integrant.repl/set-prep! core/system-config-start)
9 |
10 | (defn system [] (or state/system (throw (ex-info "System not running" {}))))
11 |
12 | (defn env [] (:backend/env (system)))
13 |
14 | (defn profile [] (:backend/profile (system)))
15 |
16 | (defn my-dummy-reset []
17 | (reset))
18 |
19 | ;; NOTE: In Cursive, Integrant hot keys are:
20 | ;; M-h: go
21 | ;; M-j: reset
22 | ;; M-k: halt
23 |
24 | ; In Calva the Integrant hot keys are:
25 | ;; C-T alt+h: go
26 | ;; M-j: reset
27 | ;; C-T alt+k: halt
28 |
29 |
30 | #_(comment
31 | (user/system)
32 | (user/env)
33 | ; alt+j hotkey in Cursive
34 | (integrant.repl/reset)
35 | (+ 1 2)
36 |
37 | )
38 |
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "clojurescript-frontend",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "concurrently 'npm:watch:cljs'",
7 | "watch:cljs": "shadow-cljs watch app"
8 | },
9 | "devDependencies": {
10 | "concurrently": "8.0.1",
11 | "shadow-cljs": "2.22.9",
12 | "postcss": "^8.4.21",
13 | "postcss-cli": "^10.1.0",
14 | "tailwindcss": "^3.3.0"
15 | },
16 | "dependencies": {
17 | "highlight.js": "11.7.0",
18 | "create-react-class": "15.7.0",
19 | "react": "18.2.0",
20 | "react-dom": "18.2.0",
21 | "@tanstack/react-table": "^8.8.4"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/resources/config.edn:
--------------------------------------------------------------------------------
1 | {; Profile injected to Integrant.
2 | :backend/profile #profile {:prod :prod
3 | :dev :dev
4 | :test :test}
5 |
6 | ; Miscellaneous options.
7 | :backend/options {:jwt {:exp #profile {:prod 1000
8 | :dev 2000
9 | :test 5000}}}
10 |
11 | :backend/env {:profile #ig/ref :backend/profile
12 | :data-dir "resources/data"
13 | ;; Simulate database.
14 | ;; In real life we should have the connection to the database here.
15 | :options #ig/ref :backend/options}
16 |
17 | ;; This way we inject our world (env) to the API calls.
18 | :backend/jetty {:port #profile {:dev #long #or [#env PORT 7171]
19 | :prod #long #or [#env PORT 7172]}
20 | :join? false
21 | :env #ig/ref :backend/env}
22 |
23 | :backend/nrepl {:bind #profile {:prod "0.0.0.0"
24 | :dev "localhost"}
25 | :port #profile {:dev #long #or [#env PORT 7131]
26 | :prod #long #or [#env PORT 7132]}}}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/resources/data/pg-1-products.csv:
--------------------------------------------------------------------------------
1 | 2001 1 Kalevala 3.95 Elias Lönnrot 1835 Finland Finnish
2 | 2002 1 Moby Dick 45.35 Herman Melville 1851 United States English
3 | 2003 1 Crime and Punishment 10.51 Fyodor Dostoevsky 1866 Russia Russian
4 | 2004 1 War and Peace 2.65 Leo Tolstoy 1867 Russia Russian
5 | 2005 1 The Idiot 13.42 Fyodor Dostoevsky 1869 Russia Russian
6 | 2006 1 Seitsemän veljestä 7.22 Aleksis Kivi 1870 Finland Finnish
7 | 2007 1 Anna Karenina 21.11 Leo Tolstoy 1877 Russia Russian
8 | 2008 1 The Brothers Karamazov 7.83 Fyodor Dostoevsky 1880 Russia Russian
9 | 2009 1 The Adventures of Huckleberry Finn 61.83 Mark Twain 1884 United States English
10 | 2010 1 Buddenbrooks 58.69 Thomas Mann 1901 Germany German
11 | 2011 1 The Trial 57.22 Franz Kafka 1925 Czechoslovakia German
12 | 2012 1 Mrs Dalloway 7 Virginia Woolf 1925 United Kingdom English
13 | 2013 1 The Castle 15.84 Franz Kafka 1926 Czechoslovakia German
14 | 2014 1 The Sound and the Fury 7.22 William Faulkner 1929 United States English
15 | 2015 1 The Stranger 22.22 Albert Camus 1942 Algeria, French Empire French
16 | 2016 1 Pippi Longstocking 54.61 Astrid Lindgren 1945 Sweden Swedish
17 | 2017 1 Sinuhe egyptiläinen 13.42 Mika Waltari 1945 Finland Finnish
18 | 2018 1 Zorba the Greek 29.06 Nikos Kazantzakis 1946 Greece Greek
19 | 2019 1 Nineteen Eighty-Four 3.95 George Orwell 1949 United Kingdom English
20 | 2020 1 Tales 50.6 Edgar Allan Poe 1950 United States English
21 | 2021 1 The Old Man and the Sea 30.63 Ernest Hemingway 1952 United States English
22 | 2022 1 Havukka-ahon ajattelija 58.69 Veikko Huovinen 1952 Finland Finnish
23 | 2023 1 Tuntematon sotilas 21.11 Väinö Linna 1954 Finland Finnish
24 | 2024 1 Taikatalvi 15.84 Tove Jansson 1957 Finland Finnish
25 | 2025 1 The Tin Drum 22.12 Günter Grass 1959 Germany German
26 | 2026 1 Punainen viiva 7 Ilmari Kianto 1959 Finland Finnish
27 | 2027 1 Täällä Pohjantähden alla 29.06 Väinö Linna 1959 Finland Finnish
28 | 2028 1 Maa on syntinen laulu 61.83 Timo K. Mukka 1964 Finland Finnish
29 | 2029 1 One Hundred Years of Solitude 10.66 Gabriel García Márquez 1967 Colombia Spanish
30 | 2030 1 Manillaköysi 57.22 Veijo Meri 1967 Finland Finnish
31 | 2031 1 Simpauttaja 7.83 Heikki Turunen 1973 Finland Finnish
32 | 2032 1 Jäniksen vuosi 22.22 Arto Paasilinna 1975 Finland Finnish
33 | 2033 1 Blindness 10.7 José Saramago 1995 Portugal Portuguese
34 | 2034 1 Klassikko 54.61 Kari Hotakainen 1997 Finland Finnish
35 | 2035 1 Mielensäpahoittaja 50.6 Tuomas Kyrö 2010 Finland Finnish
36 |
--------------------------------------------------------------------------------
/resources/data/pg-2-products.csv:
--------------------------------------------------------------------------------
1 | 1 2 Juurakon Hulda 82.92 Valentin Vaala 1937 Finland Drama
2 | 2 2 Varastettu kuolema 161.32 Nyrki Tapiovaara 1938 Finland Drama
3 | 3 2 Rashomon 13.89 Kurosawa, Akira 1950 Japan Drama-Crime
4 | 4 2 Sunset Blvd. 122.3 Wilder, Billy 1950 USA Drama
5 | 5 2 Strangers on a Train 36.16 Hitchcock, Alfred 1951 USA Thriller-Drama-Crime
6 | 6 2 The African Queen 64.15 Huston, John 1951 USA Romance-Adventure
7 | 7 2 A Place in the Sun 18.46 Stevens, George 1951 USA Romance-Drama
8 | 8 2 Singin' in the Rain 36.88 Donen, Stanley/Gene Kelly 1952 USA Musical-Dance-Comedy
9 | 9 2 Ikiru 192.52 Kurosawa, Akira 1952 Japan Drama
10 | 10 2 Valkoinen peura 2.76 Erik Blomberg 1952 Finland Thriller-Action
11 | 11 2 Gentlemen Prefer Blondes 102.91 Hawks, Howard 1953 USA Romance-Musical-Comedy
12 | 12 2 Seven Samurai 84.97 Kurosawa, Akira 1954 Japan Drama-Action-History
13 | 13 2 Rear Window 12.91 Hitchcock, Alfred 1954 USA Thriller-Mystery
14 | 14 2 La Strada 88.26 Fellini, Federico 1954 Italy Drama
15 | 15 2 East of Eden 8.93 Kazan, Elia 1955 USA Drama
16 | 16 2 Rebel Without a Cause 5.31 Ray, Nicholas 1955 USA Drama
17 | 17 2 Tuntematon sotilas 17.36 Edvin Laine 1955 Finland War
18 | 18 2 Elokuu 0.19 Matti Kassila 1956 Finland Drama
19 | 19 2 The Seventh Seal 83.08 Bergman, Ingmar 1957 Sweden Fantasy-Drama
20 | 20 2 Sweet Smell of Success 19.51 Mackendrick, Alexander 1957 USA Drama
21 | 21 2 Nights of Cabiria 111.53 Fellini, Federico 1957 Italy-France Drama
22 | 22 2 Paths of Glory 44.51 Kubrick, Stanley 1957 USA War-Drama
23 | 23 2 Throne of Blood 4.29 Kurosawa, Akira 1957 Japan War-Drama
24 | 24 2 An Affair to Remember 5.92 McCarey, Leo 1957 USA Romance-Drama
25 | 25 2 The Bridge on the River Kwai 55.09 Lean, David 1957 UK War-Drama-Action
26 | 26 2 Some Like it Hot 6.33 Wilder, Billy 1959 USA Romance-Crime-Comedy
27 | 27 2 Rio Bravo 105.46 Hawks, Howard 1959 USA Western
28 | 28 2 Ben-Hur 38.97 Wyler, William 1959 USA Religious-History
29 | 29 2 Psycho 125.33 Hitchcock, Alfred 1960 USA Thriller-Horror
30 | 30 2 Spartacus 60.3 Kubrick, Stanley 1960 USA Adventure-History-Drama
31 | 31 2 Komisario Palmun erehdys 38.78 Matti Kassila 1960 Finland Crime-Drama
32 | 32 2 Yojimbo 0.68 Kurosawa, Akira 1961 Japan Drama-Action
33 | 33 2 Breakfast at Tiffany's 35.71 Edwards, Blake 1961 USA Romance-Drama-Comedy
34 | 34 2 Kaasua, komisario Palmu 7.34 Matti Kassila 1961 Finland Crime
35 | 35 2 The Man Who Shot Liberty Valance 46.85 Ford, John 1962 USA Western
36 | 36 2 To Kill a Mockingbird 30.22 Mulligan, Robert 1962 USA Drama
37 | 37 2 Hatari! 28.43 Hawks, Howard 1962 USA Drama
38 | 38 2 The Manchurian Candidate 25.82 Frankenheimer, John 1962 USA Political-Thriller-Drama
39 | 39 2 Ride the High Country 58.62 Peckinpah, Sam 1962 USA Western
40 | 40 2 Pojat 3.25 Mikko Niskanen 1962 Finland Drama
41 | 41 2 Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb 99.92 Kubrick, Stanley 1964 UK-USA War-Comedy
42 | 42 2 A Hard Day's Night 27.63 Lester, Richard 1964 UK Musical-Comedy
43 | 43 2 The Good, the Bad and the Ugly 12.5 Leone, Sergio 1966 Italy-Spain Western-Action
44 | 44 2 Who's Afraid of Virginia Woolf? 38.78 Nichols, Mike 1966 USA Drama
45 | 45 2 Käpy selän alla 97.13 Mikko Niskanen 1966 Finland Drama
46 | 46 2 The Graduate 27.58 Nichols, Mike 1967 USA Romance-Drama-Comedy
47 | 47 2 Bonnie and Clyde 2.76 Penn, Arthur 1967 USA Drama-Crime-Biography
48 | 48 2 2001: A Space Odyssey 17.36 Kubrick, Stanley 1968 UK-USA Science Fiction
49 | 49 2 Once Upon a Time in the West 14.4 Leone, Sergio 1968 Italy-USA Western
50 | 50 2 Rosemary's Baby 37.22 Polanski, Roman 1968 USA Thriller-Horror-Drama
51 | 51 2 Night of the Living Dead 21.9 Romero, George A. 1968 USA Horror
52 | 52 2 The Wild Bunch 0.66 Peckinpah, Sam 1969 USA Western
53 | 53 2 Easy Rider 69.02 Hopper, Dennis 1969 USA Drama
54 | 54 2 Ruusujen aika 24.29 Risto Jarva 1969 Finland Drama
55 | 55 2 M*A*S*H 17.5 Altman, Robert 1970 USA War-Comedy
56 | 56 2 Naisenkuvia 136.06 Jörn Donner 1970 Finland Drama
57 | 57 2 Dirty Harry 166 Siegel, Don 1971 USA Crime-Action
58 | 58 2 Solaris 96.76 Tarkovsky, Andrei 1972 USSR Science Fiction-Drama-Mystery
59 | 59 2 Last Tango in Paris 33.7 Bertolucci, Bernardo 1972 France-Italy Romance-Drama-Adult
60 | 60 2 Fellini's Roma 82.92 Fellini, Federico 1972 Italy Drama-Comedy
61 | 61 2 Kahdeksan surmanluotia 14.4 Mikko Niskanen 1972 Finland Thriller-Action
62 | 62 2 Kun taivas putoaa... 166.71 Risto Jarva 1972 Finland Drama
63 | 63 2 The Long Goodbye 3.25 Altman, Robert 1973 USA Drama-Crime
64 | 64 2 Pat Garrett and Billy the Kid 8.47 Peckinpah, Sam 1973 USA Western
65 | 65 2 Sensuela 69.02 Teuvo Tulio 1973 Finland Erotic
66 | 66 2 Maa on syntinen laulu 166 Rauni Mollberg 1973 Finland Drama
67 | 67 2 Yhden miehen sota 3.79 Risto Jarva 1973 Finland War
68 | 68 2 Bring Me the Head of Alfredo Garcia 14.7 Peckinpah, Sam 1974 USA Crime-Thriller-Action
69 | 69 2 One Flew Over the Cuckoo's Nest 0.19 Forman, Milos 1975 USA Drama
70 | 70 2 Monty Python and the Holy Grail 4.28 Gilliam, Terry/Terry Jones 1975 UK History-Comedy
71 | 71 2 Taxi Driver 42.05 Scorsese, Martin 1976 USA Drama-Crime
72 | 72 2 The Outlaw Josey Wales 103.31 Eastwood, Clint 1976 USA Western-Drama
73 | 73 2 Loma 4.28 Risto Jarva 1976 Finland Comedy
74 | 74 2 Star Wars 66.77 Lucas, George 1977 USA Science Fiction-Fantasy-Adventure
75 | 75 2 Close Encounters of the Third Kind 72.74 Spielberg, Steven 1977 USA Science Fiction-Adventure
76 | 76 2 Jäniksen vuosi 0.66 Risto Jarva 1977 Finland Comedy
77 | 77 2 The Deer Hunter 161.32 Cimino, Michael 1978 USA War-Drama
78 | 78 2 Apocalypse Now 13.53 Coppola, Francis Ford 1979 USA War-Drama-Adventure
79 | 79 2 Stalker 7.34 Tarkovsky, Andrei 1979 USSR Science Fiction-Mystery
80 | 80 2 Alien 9.64 Scott, Ridley 1979 USA Science Fiction-Horror
81 | 81 2 Monty Python's Life of Brian 4.79 Jones, Terry 1979 UK Religious-Comedy
82 | 82 2 Raging Bull 166.71 Scorsese, Martin 1980 USA Sport-Drama-Biography
83 | 83 2 The Shining 97.13 Kubrick, Stanley 1980 USA Horror
84 | 84 2 The Empire Strikes Back 167.68 Kershner, Irvin 1980 USA Science Fiction-Fantasy-Adventure
85 | 85 2 Kagemusha 136.06 Kurosawa, Akira 1980 Japan War-Drama-History
86 | 86 2 Täältä tullaan elämä! 72.74 Tapio Suominen 1980 Finland Drama
87 | 87 2 Raiders of the Lost Ark 6.97 Spielberg, Steven 1981 USA Adventure-Action
88 | 88 2 Mad Max 2 56.65 Miller, George 1981 Australia Science Fiction-Action-Adventure
89 | 89 2 Excalibur 16.99 Boorman, John 1981 UK Fantasy-Adventure-Action
90 | 90 2 Das Boot 24.29 Petersen, Wolfgang 1981 West Germany Action-Adventure-Drama
91 | 91 2 Reds 12.11 Beatty, Warren 1981 USA History-Drama-Biography
92 | 92 2 Blade Runner 3.79 Scott, Ridley 1982 USA Science Fiction-Thriller-Drama
93 | 93 2 Fanny and Alexander 43.33 Bergman, Ingmar 1982 Sweden Drama
94 | 94 2 E.T. The Extra-Terrestrial 125.49 Spielberg, Steven 1982 USA Science Fiction-Family
95 | 95 2 The Thing 46.87 Carpenter, John 1982 USA Science Fiction-Action-Horror
96 | 96 2 Arvottomat 33.7 Mika Kaurismäki 1982 Finland Drama
97 | 97 2 Scarface 22.71 De Palma, Brian 1983 USA Drama-Crime
98 | 98 2 Once Upon a Time in America 5.59 Leone, Sergio 1984 Italy-USA Drama-Crime
99 | 99 2 Paris, Texas 71.21 Wenders, Wim 1984 USA Drama
100 | 100 2 This is Spinal Tap 17.71 Reiner, Rob 1984 USA Comedy-Musical
101 | 101 2 Amadeus 38.18 Forman, Milos 1984 USA Musical-Biography-Drama
102 | 102 2 The Terminator 23.74 Cameron, James 1984 USA Science Fiction-Action
103 | 103 2 Brazil 19.3 Gilliam, Terry 1985 UK Science Fiction-Fantasy
104 | 104 2 Ran 0.8 Kurosawa, Akira 1985 France-Japan War-History-Drama
105 | 105 2 Back to the Future 35.21 Zemeckis, Robert 1985 USA Science Fiction-Comedy
106 | 106 2 Calamari Union 17.5 Aki Kaurismäki 1985 Finland Drama
107 | 107 2 Tuntematon sotilas 96.76 Rauni Mollberg 1985 Finland War
108 | 108 2 Blue Velvet 102.42 Lynch, David 1986 USA Mystery-Crime-Drama
109 | 109 2 Varjoja paratiisissa 8.47 Aki Kaurismäki 1986 Finland Drama
110 | 110 2 Lumikuningatar 167.68 Päivi Hartzell 1986 Finland Drama
111 | 111 2 Full Metal Jacket 127 Kubrick, Stanley 1987 USA War-Drama-Action
112 | 112 2 Die Hard 7.74 McTiernan, John 1988 USA Thriller-Action
113 | 113 2 The Last Temptation of Christ 1.49 Scorsese, Martin 1988 USA-Canada Religious-Drama
114 | 114 2 Akira 32.64 Otomo, Katsuhiro 1988 Japan Animated-Science Fiction-Action
115 | 115 2 Katsastus 9.64 Matti Ijäs 1988 Finland Drama
116 | 116 2 Do the Right Thing 103.94 Lee, Spike 1989 USA Drama-Comedy
117 | 117 2 The Cook, the Thief, His Wife & Her Lover 0.62 Greenaway, Peter 1989 UK-France Drama
118 | 118 2 Dead Poets Society 4.55 Weir, Peter 1989 USA Drama
119 | 119 2 GoodFellas 52.56 Scorsese, Martin 1990 USA Drama-Crime
120 | 120 2 Miller's Crossing 24.24 Coen, Joel & Ethan Coen 1990 USA Crime
121 | 121 2 Akira Kurosawa's Dreams 150.53 Kurosawa, Akira 1990 USA Fantasy
122 | 122 2 Tulitikkutehtaan tyttö 66.77 Aki Kaurismäki 1990 Finland Drama
123 | 123 2 The Silence of the Lambs 11.04 Demme, Jonathan 1991 USA Thriller-Mystery
124 | 124 2 JFK 49.91 Stone, Oliver 1991 USA History-Drama
125 | 125 2 Thelma & Louise 83.62 Scott, Ridley 1991 USA Drama-Comedy-Adventure
126 | 126 2 Zombie ja kummitusjuna 43.33 Mika Kaurismäki 1991 Finland Drama
127 | 127 2 Unforgiven 23.45 Eastwood, Clint 1992 USA Western
128 | 128 2 Reservoir Dogs 93.73 Tarantino, Quentin 1992 USA Drama-Crime-Thriller
129 | 129 2 Tuhlaajapoika 103.31 Veikko Aaltonen 1992 Finland Drama
130 | 130 2 Takaisin ryssiin 12.11 Jari Halonen 1992 Finland Drama
131 | 131 2 The Piano 0.22 Campion, Jane 1993 New Zealand-Australia-France History-Drama-Romance
132 | 132 2 Schindler's List 3.78 Spielberg, Steven 1993 USA War-History-Drama
133 | 133 2 Groundhog Day 30.52 Ramis, Harold 1993 USA Romance-Fantasy-Comedy
134 | 134 2 Pulp Fiction 64.93 Tarantino, Quentin 1994 USA Drama-Crime-Comedy
135 | 135 2 Forrest Gump 45.49 Zemeckis, Robert 1994 USA Drama-Comedy
136 | 136 2 Léon 20.64 Besson, Luc 1994 France Thriller-Crime
137 | 137 2 Heat 51.55 Mann, Michael 1995 USA Thriller-Drama-Crime
138 | 138 2 Casino 49.07 Scorsese, Martin 1995 USA Drama-Crime
139 | 139 2 The Bridges of Madison County 44.5 Eastwood, Clint 1995 USA Romance-Drama
140 | 140 2 The Usual Suspects 14.98 Singer, Bryan 1995 USA Thriller-Mystery-Crime
141 | 141 2 Fargo 3.86 Coen, Joel & Ethan Coen 1996 USA Drama-Crime-Comedy
142 | 142 2 Trainspotting 0.54 Boyle, Danny 1996 UK Drama-Comedy
143 | 143 2 Kauas pilvet karkaavat 27.58 Aki Kaurismäki 1996 Finland Drama
144 | 144 2 L.A. Confidential 3.83 Hanson, Curtis 1997 USA Thriller-Crime
145 | 145 2 Starship Troopers 5.95 Verhoeven, Paul 1997 USA Science Fiction-Action-Adventure-War
146 | 146 2 Neitoperho 42.05 Auli Mantila 1997 Finland Drama
147 | 147 2 Magnolia 3.47 Anderson, Paul Thomas 1999 USA Drama
148 | 148 2 The Matrix 114.7 Wachowski, Lana & Lilly Wachowski 1999 USA-Australia Science Fiction-Action-Adventure
149 | 149 2 Eyes Wide Shut 23.85 Kubrick, Stanley 1999 USA-UK Thriller-Drama
150 | 150 2 Fight Club 6.8 Fincher, David 1999 USA-Germany Drama
151 | 151 2 American Beauty 139.87 Mendes, Sam 1999 USA Drama-Comedy
152 | 152 2 Pitkä kuuma kesä 6.97 Perttu Leppä 1999 Finland Drama
153 | 153 2 Mulholland Dr. 3.33 Lynch, David 2001 France-USA Mystery-Drama-Thriller
154 | 154 2 A.I. Artificial Intelligence 8.63 Spielberg, Steven 2001 USA Science Fiction-Drama
155 | 155 2 Amélie 183.34 Jeunet, Jean-Pierre 2001 France-Germany Romance-Comedy
156 | 156 2 Mies vailla menneisyyttä 37.22 Aki Kaurismäki 2002 Finland Drama
157 | 157 2 Aleksis Kiven elämä 14.7 Jari Halonen 2002 Finland Drama
158 | 158 2 Million Dollar Baby 37.11 Eastwood, Clint 2004 USA Drama
159 | 159 2 Koirankynnen leikkaaja 4.79 Markku Pölönen 2004 Finland Drama
160 | 160 2 Brokeback Mountain 134.34 Lee, Ang 2005 USA-Canada Romance-Drama
161 | 161 2 A History of Violence 23.66 Cronenberg, David 2005 USA-Germany Crime-Drama-Mystery
162 | 162 2 Paha maa 21.9 Aku Louhimies 2005 Finland Drama
163 | 163 2 Äideistä parhain 125.49 Klaus Härö 2005 Finland Drama
164 | 164 2 There Will Be Blood 117.9 Anderson, Paul Thomas 2007 USA Drama
165 | 165 2 No Country for Old Men 19.93 Coen, Joel & Ethan Coen 2007 USA Crime-Drama
166 | 166 2 Avatar 26.83 Cameron, James 2009 USA-UK Action-Science Fiction-Adventure
167 | 167 2 Postia pappi Jaakobille 56.65 Klaus Härö 2009 Finland Drama
168 | 168 2 Rare Exports 16.99 Jalmari Helander 2010 Finland Thriller-Action
169 | 169 2 Hyvä poika 13.53 Zaida Bergroth 2011 Finland Drama
170 |
--------------------------------------------------------------------------------
/resources/data/product-groups.csv:
--------------------------------------------------------------------------------
1 | 1 Books
2 | 2 Movies
3 |
--------------------------------------------------------------------------------
/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
8 |
9 |
10 |
11 |
12 |
29 |
30 |
31 | logs/backend.log
32 |
33 |
34 | logs/archived/app.%d{yyyy-MM-dd}.%i.log.gz
35 |
36 | 10MB
37 |
38 | 20MB
39 |
40 | 30
41 |
42 |
43 |
44 | %d %p [%t] %m%n
45 |
46 |
47 |
48 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/resources/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karimarttila/clojure-full-stack-demo/5520c0123d6f6bc1c186b7dc94a6e367768417bb/resources/public/favicon.ico
--------------------------------------------------------------------------------
/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Clojure-full-stack-demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/resources/public/info.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Simple Server
6 |
7 | This is the Simple Server.
8 |
9 | Available commands are:
10 |
11 |
12 | - GET / - The Frontend.
13 | - GET /info.html - This file.
14 | - GET /api/info - Gives general information.
15 | - GET /api/ping - Ping test.
16 | - POST /api/ping - Ping test (expects to find parameter ping in body).
17 | - POST /api/sign-in - Creates a new user.
18 | - POST /api/login - Logs on to service using credentials created using sign-in. You get json web token that must be supplied in following requests.
19 | - GET /api/products-groups - Gets product groups.
20 | - GET /api/products/ - Gets products in product group.
21 | - GET /api/product/ - Gets specific product information.
22 |
23 |
24 | NOTE: You can see examples how to call these endpoints in the scripts directory (using curl).
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/scripts/get-ping.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | http http://localhost:7171/api/ping
4 |
--------------------------------------------------------------------------------
/scripts/info.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | http http://localhost:7171/api/info
3 |
--------------------------------------------------------------------------------
/scripts/login.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo '{"username": "jartsa", "password": "joo"}' | http --json POST localhost:7171/api/login
4 |
5 | #http POST http://localhost:7171/api/login username=jartsa password=joo Content-Type:application/json
6 |
7 | #curl -H "Content-Type: application/json" -X POST -d '{"username": "jartsa", "password": "joo"}' http://localhost:7171/api/login
8 |
--------------------------------------------------------------------------------
/scripts/post-ping.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | curl -H "Content-Type: application/json" -X POST -d '{"ping":"Jee!"}' http://localhost:7171/api/ping
--------------------------------------------------------------------------------
/scripts/product-2-49.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | RET=$(http POST http://localhost:7171/api/login username=jartsa password=joo Content-Type:application/json)
4 | TOKEN=$(echo $RET | jq '.token' | tr -d '"')
5 | #echo $TOKEN
6 |
7 |
8 | http http://localhost:7171/api/product/2/49 x-token:$TOKEN
--------------------------------------------------------------------------------
/scripts/product-groups.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | RET=$(http POST http://localhost:7171/api/login username=jartsa password=joo Content-Type:application/json)
4 | TOKEN=$(echo $RET | jq '.token' | tr -d '"')
5 | #echo $TOKEN
6 |
7 | #http http://localhost:7171/api/product-groups
8 | #http http://localhost:7171/api/product-groups x-token:"WRONG-TOKEN"
9 | http http://localhost:7171/api/product-groups x-token:$TOKEN
10 |
--------------------------------------------------------------------------------
/scripts/products-1.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | RET=$(http POST http://localhost:7171/api/login username=jartsa password=joo Content-Type:application/json)
4 | TOKEN=$(echo $RET | jq '.token' | tr -d '"')
5 | #echo $TOKEN
6 |
7 | http http://localhost:7171/api/products/1 x-token:$TOKEN
8 |
--------------------------------------------------------------------------------
/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | ;; shadow-cljs configuration
2 | {:deps {:aliases [:dev :common :backend :frontend :test :kari :calva]}
3 | :dependencies []
4 | :builds {:app {:target :browser
5 | :output-dir "dev-resources/public/js"
6 | :asset-path "/js"
7 | :devtools {:watch-dir "dev-resources/public"
8 | :preloads [hashp.core]
9 | ;:preloads [devtools.preload
10 | ; reagent-dev-tools.preload
11 | ; day8.re-frame-10x.preload
12 | ; ]
13 | }
14 | :modules {:main {:entries [frontend.main]}}
15 | :release {:output-dir "prod-resources/public/js"
16 | :compiler-options {:source-map "prod-resources/public/js/main.js.map"
17 | :optimizations :advanced}}
18 | ;:compiler-options {:infer-externs :auto
19 | ; :closure-defines {"re_frame.trace.trace_enabled_QMARK_" true}
20 | ; :external-config {:reagent-dev-tools {:state-atom re-frame.db.app-db}}
21 | ; }
22 | ;; :source-map true
23 | }}}
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/clj/backend/core.clj:
--------------------------------------------------------------------------------
1 | (ns backend.core
2 | (:require
3 | [clojure.tools.logging :as log]
4 | [clojure.pprint]
5 | [clojure.java.io :as io]
6 | [ring.adapter.jetty :as jetty]
7 | [nrepl.server :as nrepl]
8 | [integrant.repl :as ig-repl]
9 | [integrant.core :as ig]
10 | [aero.core :as aero]
11 | [backend.webserver :as b-webserver]
12 | [backend.db.domain :as b-domain]
13 | [clojure.tools.reader.edn :as edn]
14 | [potpuri.core :as p])
15 | (:gen-class))
16 |
17 | (defn env-value [key default]
18 | (some-> (or (System/getenv (name key)) default)))
19 |
20 | (defmethod aero/reader 'ig/ref [_ _ value] (ig/ref value))
21 |
22 | (defmethod ig/init-key :backend/profile [_ profile]
23 | profile)
24 |
25 | (defn create-users []
26 | [{:username "jartsa" :password "joo"}
27 | {:username "rane" :password "jee"}
28 | {:username "d" :password "d"}])
29 |
30 | (defmethod ig/init-key :backend/env [_ {:keys [_profile data-dir] :as m}]
31 | (log/debug "ENTER ig/init-key :backend/env")
32 | ;; Simulates our database.
33 | (conj m {:db (atom {:domain (b-domain/get-domain-data data-dir)
34 | :sessions #{}
35 | :users (create-users)})}))
36 |
37 | (defmethod ig/halt-key! :backend/env [_ this]
38 | (log/debug "ENTER ig/halt-key! :backend/env")
39 | this)
40 |
41 | (defmethod ig/suspend-key! :backend/env [_ this]
42 | (log/debug "ENTER ig/suspend-key! :backend/env")
43 | this)
44 |
45 | (defmethod ig/resume-key :backend/env [_ _ _ old-impl]
46 | (log/debug "ENTER ig/resume-key :backend/env")
47 | old-impl)
48 |
49 | (defmethod ig/init-key :backend/jetty [_ {:keys [port join? env]}]
50 | (log/debug "ENTER ig/init-key :backend/jetty")
51 | (-> (b-webserver/handler (b-webserver/routes env))
52 | (jetty/run-jetty {:port port :join? join?})))
53 |
54 | (defmethod ig/halt-key! :backend/jetty [_ server]
55 | (log/debug "ENTER ig/halt-key! :backend/jetty")
56 | (.stop server))
57 |
58 | (defmethod ig/init-key :backend/options [_ options]
59 | (log/debug "ENTER ig/init-key :backend/options")
60 | options)
61 |
62 | (defmethod ig/init-key :backend/nrepl [_ {:keys [bind port]}]
63 | (log/debug "ENTER ig/init-key :backend/nrepl")
64 | (if (and bind port)
65 | (nrepl/start-server :bind bind :port port)
66 | nil))
67 |
68 | (defmethod ig/halt-key! :backend/nrepl [_ this]
69 | (log/debug "ENTER ig/halt-key! :backend/nrepl")
70 | (if this
71 | (nrepl/stop-server this)))
72 |
73 | (defmethod ig/suspend-key! :backend/nrepl [_ this]
74 | (log/debug "ENTER ig/suspend-key! :backend/nrepl")
75 | this)
76 |
77 | (defmethod ig/resume-key :backend/nrepl [_ _ _ old-impl]
78 | (log/debug "ENTER ig/resume-key :backend/nrepl")
79 | old-impl)
80 |
81 | ; You can add to the container script PROFILE=prod
82 | (defn read-config [profile]
83 | (let [local-config (let [file (io/file "config-local.edn")]
84 | #_{:clj-kondo/ignore [:missing-else-branch]}
85 | (if (.exists file) (edn/read-string (slurp file))))]
86 | (cond-> (aero/read-config (io/resource "config.edn") {:profile profile})
87 | local-config (p/deep-merge local-config))))
88 |
89 | (defn system-config [myprofile]
90 | (let [profile (or myprofile (some-> (System/getenv "PROFILE") keyword) :dev)
91 | _ (log/info "Using profile " profile)
92 | config (read-config profile)]
93 | config))
94 |
95 | (defn system-config-start []
96 | (system-config nil))
97 |
98 | (defn -main []
99 | (log/info "System starting...")
100 | (let [config (system-config-start)
101 | _ (log/info "Config: " config)]
102 | (ig-repl/set-prep! (constantly config))
103 | (ig-repl/go)))
104 |
105 |
106 | #_(comment
107 | (ig-repl/reset)
108 | (ig-repl/halt)
109 | (user/system)
110 | (keys (user/system))
111 | (:backend/env (user/system))
112 | (keys (:backend/env (user/system)))
113 | (keys @(:backend/env (user/system)))
114 | (type @(:backend/env (user/system)))
115 | (:users @(:backend/env (user/system)))
116 | (:users @(:backend/env (user/system)))
117 | (user/env)
118 | (user/profile)
119 | (System/getenv)
120 | (env-value :PATH :foo)
121 | )
--------------------------------------------------------------------------------
/src/clj/backend/db/domain.clj:
--------------------------------------------------------------------------------
1 | (ns backend.db.domain
2 | "Loads the domain data from csv"
3 | (:require
4 | [clojure.tools.logging :as log]
5 | [clojure.data.csv :as csv]
6 | [clojure.java.io :as io]
7 | [clojure.string :as str])
8 | (:import (java.io FileNotFoundException)))
9 |
10 | (defn get-product-groups [data-dir]
11 | (log/debug (str "ENTER load-product-groups"))
12 | (let [raw (with-open [reader (io/reader (str data-dir "/product-groups.csv"))]
13 | (doall
14 | (csv/read-csv reader)))
15 | ;;_ #p raw
16 | ]
17 | (into [] (map
18 | (fn [[item]]
19 | (let [[pgId name] (str/split item #"\t")]
20 | {:pgId (Integer/parseInt pgId) :name name}))
21 | raw))))
22 |
23 |
24 | (defn get-raw-products [data-dir pg-id]
25 | (log/debug (str "ENTER get-raw-products, pg-id: " pg-id))
26 | (let [raw-products (try
27 | (with-open [reader (io/reader (str data-dir "/pg-" pg-id "-products.csv"))]
28 | (doall
29 | (csv/read-csv reader :separator \tab)))
30 | (catch FileNotFoundException _ nil))
31 | ;_ #p raw-products
32 | ]
33 | raw-products))
34 |
35 |
36 | (defn create-book [item]
37 | (try
38 | {:pId (Integer/parseInt (get item 0))
39 | :pgId (Integer/parseInt (get item 1))
40 | :title (get item 2)
41 | :price (Double/parseDouble (get item 3))
42 | :author (get item 4)
43 | :year (Integer/parseInt (get item 5))
44 | :country (get item 6)
45 | :language (get item 7)}
46 | (catch Exception e {:msg (str "Exception: " (.getMessage e))
47 | :item item})))
48 |
49 |
50 | (defn create-movie [item]
51 | (try
52 | {:pId (Integer/parseInt (get item 0))
53 | :pgId (Integer/parseInt (get item 1))
54 | :title (get item 2)
55 | :price (Double/parseDouble (get item 3))
56 | :director (get item 4)
57 | :year (Integer/parseInt (get item 5))
58 | :country (get item 6)
59 | :genre (get item 7)}
60 | (catch Exception e {:msg (str "Exception: " (.getMessage e))
61 | :item item})))
62 |
63 |
64 | (defn convert-products [raw-products]
65 | (map (fn [item]
66 | (if (= (count item) 8)
67 | (let [pg-id (Integer/parseInt (get item 1))]
68 | (cond
69 | (not= (count item) 8) nil
70 | (= pg-id 1) (create-book item)
71 | (= pg-id 2) (create-movie item)
72 | :else nil))
73 | nil)
74 | )
75 | raw-products))
76 |
77 |
78 | (defn get-domain-data [data-dir]
79 | (let [product-groups (get-product-groups data-dir)
80 | ;_ #p product-groups
81 | pg-ids (map (fn [item] (:pgId item)) product-groups)
82 | ;_ #p pg-ids
83 | raw-products (mapcat (fn [pg-id]
84 | (get-raw-products data-dir pg-id)) pg-ids)
85 | ;_ #p raw-products
86 | products (convert-products raw-products)]
87 | {:product-groups product-groups
88 | :products products}))
89 |
90 |
91 | ;(def my-atom (atom nil))
92 |
93 |
94 | (defn get-products [pg-id products]
95 | (let [;_ (reset! my-atom products)
96 | ]
97 | (filter (fn [item] (= (:pgId item) pg-id)) products)))
98 |
99 |
100 | (defn get-product [pg-id p-id products]
101 | (first (filter (fn [item] (and (= (:pgId item) pg-id) (= (:pId item) p-id))) products)))
102 |
103 |
104 |
105 |
106 | (comment
107 | ;; (count @my-atom)
108 | ;; (count (filter (fn [item] (= (:pgId item) 1)) @my-atom))
109 | ;; (count (get-products 1 @my-atom))
110 |
111 |
112 | (def my-data-dir "resources/data")
113 | (def my-product-groups (get-product-groups my-data-dir))
114 | my-product-groups
115 | (def my-pg-ids (map (fn [item] (:pgId item)) my-product-groups))
116 | my-pg-ids
117 | (def my-raw-products (mapcat (fn [pg-id]
118 | (get-raw-products my-data-dir pg-id)) my-pg-ids))
119 | my-raw-products
120 | (count (first my-raw-products))
121 | (convert-products my-raw-products)
122 |
123 | (get-domain-data "resources/data")
124 | (keys (get-domain-data "resources/data"))
125 | (count (:products (get-domain-data "resources/data")))
126 | (get-product-groups "resources/data")
127 | (get-raw-products "resources/data" 1)
128 |
129 | (let [products (:products (get-domain-data "resources/data"))]
130 | (first (filter (fn [item] (and (= (:pgId item) 2) (= (:pId item) 49))) products)))
131 |
132 | (get-product 2 49 (:products (get-domain-data "resources/data")))
133 |
134 | )
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/src/clj/backend/db/users.clj:
--------------------------------------------------------------------------------
1 | (ns backend.db.users
2 | (:require [clojure.tools.logging :as log]
3 | [buddy.sign.jwt :as buddy-jwt]
4 | [clojure.data.codec.base64 :as base64]
5 | [clj-time.core :as clj-time]))
6 |
7 |
8 | ;; Demonstration: For development purposes.
9 | (def my-atom (atom {}))
10 | #_(comment
11 | @my-atom
12 | (keys @my-atom)
13 | (:db @my-atom)
14 | (keys @(:db @my-atom))
15 | (:users @(:db @my-atom)))
16 |
17 | (def my-hex-secret
18 | "Creates dynamically a hex secret when the server boots."
19 | ((fn []
20 | (let [my-chars (->> (range (int \a) (inc (int \z))) (map char))
21 | my-ints (->> (range (int \0) (inc (int \9))) (map char))
22 | my-set (lazy-cat my-chars my-ints)
23 | hexify (fn [s]
24 | (apply str
25 | (map #(format "%02x" (int %)) s)))]
26 | (hexify (repeatedly 24 #(rand-nth my-set)))))))
27 |
28 |
29 | (defn check-password [env username password]
30 | (log/debug (str "ENTER check-password"))
31 | ;; For demonstration
32 | ;(reset! my-atom env)
33 | (log/debug (str username))
34 | (let [users (:users @(:db env))]
35 | (= 1 (count (filter (fn [user]
36 | (and (= (:username user) username) (= (:password user) password)))
37 | users)))))
38 |
39 | (defn generate-token [env username]
40 | (log/debug (str "ENTER generate-token, username: " username))
41 | (let [my-secret my-hex-secret
42 | exp-time (clj-time/plus (clj-time/now) (clj-time/seconds (get-in env [:options :jwt :exp])))
43 | my-claim {:username username :exp exp-time}
44 | json-web-token (buddy-jwt/sign my-claim my-secret)]
45 | json-web-token))
46 |
47 |
48 | (defn add-session [env session]
49 | (log/debug (str "ENTER add-session"))
50 | (let [db (:db env)]
51 | ;; First remove the old session, if exists.
52 | (let [old-session (first (filter (fn [s]
53 | (and (= (:username s) (:username session))
54 | (= (:password s) (:password session))))
55 | (:sessions @db)))]
56 | (when old-session
57 | (swap! db update-in [:sessions] disj old-session)))
58 | ;; Then add a new session.
59 | (swap! db update-in [:sessions] conj session)))
60 |
61 |
62 | ;; For testing purposes.
63 | (defn clear-sessions [env]
64 | (log/debug (str "ENTER clear-sessions"))
65 | (let [db (:db env)]
66 | ;; Just reset :sessions key with an empty set.
67 | (swap! db assoc :sessions #{})))
68 |
69 | ;; For testing purposes.
70 | (defn get-sessions [env]
71 | (log/debug (str "ENTER clear-sessions"))
72 | (let [db (:db env)]
73 | (:sessions @db)))
74 |
75 |
76 | (defn validate-user [env username password]
77 | (let [token (if (check-password env username password)
78 | (generate-token env username)
79 | nil)]
80 | (when token
81 | (let [session {:username username :password password :token token}]
82 | (add-session env session)))
83 | token))
84 |
85 |
86 | (defn validate-token
87 | [env token]
88 | (log/debug (str "ENTER validate-token, token: " token))
89 | (let [;_ (reset! my-atom token)
90 | db (:db env)
91 | session (first (filter (fn [s] (= (:token s) token))
92 | (:sessions @db)))]
93 | ;; Part #1 of validation.
94 | (if (nil? session)
95 | (do
96 | (log/warn (str "Token not found in the session database - unknown token: " token))
97 | nil)
98 | ;; Part #2 of validation.
99 | (let [decoded-token (try
100 | (buddy-jwt/unsign token my-hex-secret)
101 | (catch Exception e
102 | (if (.contains (.getMessage e) "Token is expired")
103 | (do
104 | (log/debug (str "Token is expired, removing it from my sessions and returning nil: " token))
105 | ; TODO : remove token
106 | nil)
107 | ; Some other issue.
108 | (do
109 | (log/error (str "Some unknown exception when handling expired token, exception: " (.getMessage e)) ", token: " token)
110 | nil))))]
111 | (if decoded-token
112 | (if (= (:username session) (:username decoded-token))
113 | decoded-token
114 | (do
115 | (log/error (str "Token username does not match the session username"))
116 | nil))
117 | nil)))))
118 |
119 |
120 | (comment
121 | @my-atom
122 | (buddy-jwt/unsign @my-atom my-hex-secret))
123 |
124 |
125 | #_(comment
126 | (keys (user/env))
127 | (:db (user/env))
128 | (keys @(:db (user/env)))
129 | (:users @(:db (user/env)))
130 | (:sessions @(:db (user/env)))
131 | (def users (:users @(:db (user/env))))
132 | users
133 | (filter (fn [user]
134 | (and (= (:username user) "jartsa") (= (:password user) "joo")))
135 | users)
136 | (= 1 (count (filter (fn [user]
137 | (and (= (:username user) "jartsa") (= (:password user) "joo")))
138 | users)))
139 | ;; Now move expression to the function above (and change hardcoded valules to function arguments).
140 | (check-password (user/env) "jartsa" "joo")
141 | (check-password (user/env) "jartsa" "WRONG")
142 | @(:db (user/env))
143 |
144 | (:sessions @(:db (user/env)))
145 | (def sessions (:sessions @(:db (user/env))))
146 | sessions
147 | (def session {:username "jartsa",
148 | :password "joo",
149 | :token
150 | "JEEE"})
151 | (filter (fn [s]
152 | (and (= (:username s) (:username session)) (= (:password s) (:password session))))
153 | (:sessions @(:db (user/env))))
154 |
155 | (def mydb (atom @(:db (user/env))))
156 | mydb
157 | (def old-session (first (filter (fn [s]
158 | (and (= (:username s) (:username session)) (= (:password s) (:password session))))
159 | (:sessions @mydb))))
160 |
161 | (def session {:username "jartsa",
162 | :password "joo",
163 | :token
164 | "JEEE"})
165 |
166 | (def mydb (atom {:sessions #{session},
167 | :users [{:username "jartsa", :password "joo"}]}))
168 |
169 |
170 |
171 | @mydb
172 | (swap! mydb update-in [:sessions] disj session)
173 |
174 | (get-sessions (user/env))
175 | (add-session (user/env) {:token "jee"})
176 | (clear-sessions (user/env))
177 |
178 | )
--------------------------------------------------------------------------------
/src/clj/backend/webserver.clj:
--------------------------------------------------------------------------------
1 | (ns backend.webserver
2 | (:require [clojure.tools.logging :as log]
3 | [clojure.string :as string]
4 | [clojure.data.codec.base64 :as base64]
5 | [ring.util.http-response :as ring-response]
6 | [reitit.ring :as reitit-ring]
7 | [reitit.coercion.malli]
8 | [reitit.swagger :as reitit-swagger]
9 | [reitit.swagger-ui :as reitit-swagger-ui]
10 | [reitit.ring.malli]
11 | [reitit.ring.coercion :as reitit-coercion]
12 | [reitit.ring.middleware.muuntaja :as reitit-muuntaja]
13 | [reitit.ring.middleware.exception :as reitit-exception]
14 | [reitit.ring.middleware.parameters :as reitit-parameters]
15 | [reitit.ring.middleware.dev]
16 | [muuntaja.core :as mu-core]
17 | [backend.db.users :as b-users]
18 | [backend.db.domain :as b-domain]))
19 |
20 |
21 | (defn make-response [response-value]
22 | (if (= (:ret response-value) :ok)
23 | (ring-response/ok response-value)
24 | (ring-response/bad-request response-value)))
25 |
26 |
27 | (defn info
28 | "Gets the info."
29 | [_]
30 | (log/debug "ENTER info")
31 | {:status 200 :body {:info "/info.html => Info in HTML format"}})
32 |
33 | (defn validate-parameters
34 | "Extremely simple validator - just checks that all fields must have some value.
35 | `field-values` - a list of fields to validate."
36 | [field-values]
37 | (every? #(seq %) field-values))
38 |
39 | (defn make-response [response-value]
40 | (if (= (:ret response-value) :ok)
41 | (ring-response/ok response-value)
42 | (ring-response/bad-request response-value)))
43 |
44 |
45 | (defn login
46 | "Provides API for login page."
47 | [env username password]
48 | (log/debug "ENTER login")
49 | (let [validation-passed (validate-parameters [username password])
50 | token (if validation-passed
51 | (b-users/validate-user env username password)
52 | nil)
53 | response-value (if (not validation-passed)
54 | {:ret :failed :msg "Validation failed - some fields were empty"}
55 | (if (not token)
56 | {:ret :failed :msg "Login failed"}
57 | {:ret :ok :token token}))]
58 | (make-response response-value)))
59 |
60 |
61 | (defn product-groups
62 | "Gets product groups."
63 | [env token]
64 | (log/debug "ENTER -product-groups")
65 | (let [token-ok (b-users/validate-token env token)
66 | response-value (if token-ok
67 | (let [db (:db env)
68 | domain (:domain @db)
69 | product-groups (:product-groups domain)]
70 | {:ret :ok, :product-groups product-groups})
71 | {:ret :failed, :msg "Given token is not valid"})]
72 | (make-response response-value)))
73 |
74 | (defn products
75 | "Gets products."
76 | [env token pg-id]
77 | (log/debug "ENTER products")
78 | (let [token-ok (b-users/validate-token env token)
79 | response-value (if token-ok
80 | (let [db (:db env)
81 | domain (:domain @db)
82 | all-products (:products domain)
83 | pg-products (b-domain/get-products pg-id all-products)]
84 | {:ret :ok, :pgId pg-id :products pg-products})
85 | {:ret :failed, :msg "Given token is not valid"})]
86 | (make-response response-value)))
87 |
88 |
89 | (defn product
90 | "Gets product."
91 | [env token pg-id p-id]
92 | (log/debug "ENTER product")
93 | (let [token-ok (b-users/validate-token env token)
94 | response-value (if token-ok
95 | (let [db (:db env)
96 | domain (:domain @db)
97 | all-products (:products domain)
98 | my-product (b-domain/get-product pg-id p-id all-products)]
99 | {:ret :ok, :pgId pg-id :pId p-id :product my-product})
100 | {:ret :failed, :msg "Given token is not valid"})]
101 | (make-response response-value)))
102 |
103 | ;; UI is in http://localhost:7171/index.html
104 | (defn routes
105 | "Routes."
106 | [env]
107 | ;; http://localhost:7171/swagger.json
108 | [["/swagger.json"
109 | {:get {:no-doc true
110 | :swagger {:info {:title "simpleserver api"
111 | :description "SimpleServer Api"}
112 | :tags [{:name "api", :description "api"}]}
113 | :handler (reitit-swagger/create-swagger-handler)}}]
114 | ;; http://localhost:7171/api-docs/index.html
115 | ["/api-docs/*"
116 | {:get {:no-doc true
117 | :handler (reitit-swagger-ui/create-swagger-ui-handler
118 | {:config {:validatorUrl nil}
119 | :url "/swagger.json"})}}]
120 | ["/api"
121 | {:swagger {:tags ["api"]}}
122 | ; For development purposes. Try curl http://localhost:7171/api/ping
123 | ["/ping" {:get {:summary "ping get"
124 | ; Don't allow any query parameters.
125 | :parameters {:query [:map]}
126 | :responses {200 {:description "Ping success"}}
127 | :handler (constantly (make-response {:ret :ok, :reply "pong" :env env}))}
128 | :post {:summary "ping post"
129 | :responses {200 {:description "Ping success"}}
130 | ;; reitit adds mt/strip-extra-keys-transformer - probably changes in reitit 1.0,
131 | ;; and therefore {:closed true} is not used with reitit < 1.0.
132 | :parameters {:body [:map {:closed true} [:ping string?]]}
133 | :handler (fn [req]
134 | (let [body (get-in req [:parameters :body])
135 | myreq (:ping body)]
136 | (-> {:ret :ok :request myreq :reply "pong"}
137 | (make-response))))}}]
138 | ["/info" {:get {:summary "Get info regarding the api"
139 | :parameters {:query [:map]}
140 | :responses {200 {:description "Info success"}}
141 | :handler (fn [{}] (info env))}}]
142 | ["/login" {:post {:summary "Login to the web-store"
143 | :responses {200 {:description "Login success"}}
144 | :parameters {:body [:map
145 | [:username string?]
146 | [:password string?]]}
147 | :handler (fn [req]
148 | (let [body (get-in req [:parameters :body])
149 | {:keys [username password]} body]
150 | (login env username password)))}}]
151 | ["/product-groups" {:get {:summary "Get products groups"
152 | :responses {200 {:description "Product groups success"}}
153 | :parameters {:query [:map]}
154 | :handler (fn [req]
155 | (let [token (get-in req [:headers "x-token"])]
156 | (if (not token)
157 | (make-response {:ret :failed, :msg "Token missing in request"})
158 | (product-groups env token))))}}]
159 | ["/products/:pg-id" {:get {:summary "Get products"
160 | :responses {200 {:description "Products success"}}
161 | :parameters {:query [:map]
162 | :path [:map [:pg-id string?]]}
163 | :handler (fn [req]
164 | (let [token (get-in req [:headers "x-token"])
165 | pg-id (Integer/parseInt (get-in req [:parameters :path :pg-id]))]
166 | (if (not token)
167 | (make-response {:ret :failed, :msg "Token missing in request"})
168 | (products env token pg-id))))}}]
169 | ["/product/:pg-id/:p-id" {:get {:summary "Get product"
170 | :responses {200 {:description "Product success"}}
171 | :parameters {:query [:map]
172 | :path [:map [:pg-id string?] [:p-id string?]]}
173 | :handler (fn [req]
174 | (let [token (get-in req [:headers "x-token"])
175 | pg-id (Integer/parseInt (get-in req [:parameters :path :pg-id]))
176 | p-id (Integer/parseInt (get-in req [:parameters :path :p-id]))]
177 | (if (not token)
178 | (make-response {:ret :failed, :msg "Token missing in request"})
179 | (product env token pg-id p-id))))}}]]])
180 |
181 | ;; NOTE: If you want to check what middleware does you can uncomment rows 67-69 in:
182 | ;; https://github.com/metosin/reitit/blob/master/examples/ring-swagger/src/example/server.clj#L67-L69
183 |
184 | (defn handler
185 | "Handler."
186 | [routes]
187 | (->
188 | (reitit-ring/ring-handler
189 | (reitit-ring/router routes {; Use this to debug middleware handling:
190 | ;:reitit.middleware/transform reitit.ring.middleware.dev/print-request-diffs
191 | :data {:muuntaja mu-core/instance
192 | :coercion (reitit.coercion.malli/create
193 | {;; set of keys to include in error messages
194 | :error-keys #{:type :coercion :in #_:schema #_:value #_:errors :humanized #_:transformed}
195 | ;; validate request & response
196 | :validate true
197 | ;; top-level short-circuit to disable request & response coercion
198 | :enabled true
199 | ;; strip-extra-keys (effects only predefined transformers)
200 | :strip-extra-keys true
201 | ;; add/set default values
202 | :default-values true
203 | ;; malli options
204 | :options nil}) ;; malli
205 | :middleware [;; swagger feature
206 | reitit-swagger/swagger-feature
207 | ;; query-params & form-params
208 | reitit-parameters/parameters-middleware
209 | ;; content-negotiation
210 | reitit-muuntaja/format-negotiate-middleware
211 | ;; encoding response body
212 | reitit-muuntaja/format-response-middleware
213 | ;; exception handling
214 | (reitit-exception/create-exception-middleware
215 | (merge
216 | reitit-exception/default-handlers
217 | {::reitit-exception/wrap (fn [handler ^Exception e request]
218 | (log/error e (.getMessage e))
219 | (handler e request))}))
220 | ;; decoding request body
221 | reitit-muuntaja/format-request-middleware
222 | ;; coercing response bodys
223 | reitit-coercion/coerce-response-middleware
224 | ;; coercing request parameters
225 | reitit-coercion/coerce-request-middleware]}})
226 | (reitit-ring/routes
227 | (reitit-ring/redirect-trailing-slash-handler)
228 | (reitit-ring/create-file-handler {:path "/", :root "target/shadow/dev/resources/public"})
229 | (reitit-ring/create-resource-handler {:path "/"})
230 | (reitit-ring/create-default-handler)))))
231 |
232 | ; Rich comment.
233 | #_(comment
234 |
235 | (require '[clj-http.client])
236 |
237 | (clj-http.client/get
238 | (str "http://localhost:7171/api/info") {:debug true :accept "application/json"})
239 |
240 | (clj-http.client/get
241 | (str "http://localhost:7171/index.html") {:debug true})
242 |
243 | (clj-http.client/get
244 | (str "http://localhost:7171") {:debug true})
245 |
246 | (clj-http.client/get
247 | (str "http://localhost:7171/index.html") {:debug true})
248 |
249 | (user/system)
250 | (handler {:routes (routes (user/env))})
251 |
252 | (clj-http.client/get
253 | (str "https://reqres.in/api/users/2") {:debug true :accept "application/json"})
254 |
255 | (clj-http.client/get
256 | (str "http://localhost:7171/info") {:debug true :accept "application/edn"}))
257 |
258 |
259 |
--------------------------------------------------------------------------------
/src/cljs/frontend/header.cljs:
--------------------------------------------------------------------------------
1 | (ns frontend.header
2 | (:require [re-frame.core :as re-frame]
3 | [cljs.pprint]
4 | [frontend.state :as f-state]))
5 |
6 |
7 | (defn header []
8 | (fn []
9 | (let [login-status @(re-frame/subscribe [::f-state/login-status])
10 | username @(re-frame/subscribe [::f-state/username])]
11 | [:div.flex.grow.bg-gray-200.p-4
12 | [:div.flex.flex-col.grow
13 | [:div.flex.justify-end
14 | (when (= login-status :logged-in)
15 | [:div.flex.justify-right.gap-2
16 | [:p username]
17 | ;; NOTE: CSS for a is defined in app.css
18 | [:a {:on-click #(re-frame/dispatch [::f-state/logout]) } "Logout"]])]
19 | [:div.flex.justify-center
20 | [:h1.text-3xl.text-center.font-bold "Demo Webtore"]]]
21 | ])))
22 |
23 |
24 | ;:className "font-medium text-blue-600 dark:text-blue-500 hover:underline"
25 |
--------------------------------------------------------------------------------
/src/cljs/frontend/http.cljs:
--------------------------------------------------------------------------------
1 | (ns frontend.http
2 | (:require [frontend.util :as f-util]
3 | [ajax.core :as ajax :refer []])) ; NOTE: Empty refer for clj-kondo
4 |
5 | (defn get-headers [db]
6 | (let [token (get-in db [:token])
7 | ret (cond-> {:Accept "application/json" :Content-Type "application/json"}
8 | token (assoc :x-token token))
9 | _ (f-util/clog "get-headers, ret" ret)]
10 | ret))
11 |
12 | ;; See: https://github.com/day8/re-frame-http-fx
13 | (defn http [method db uri data on-success on-failure]
14 | (f-util/clog "http, uri" uri)
15 | (let [xhrio (cond-> {:debug true
16 | :method method
17 | :uri uri
18 | :headers (get-headers db)
19 | :format (ajax/json-request-format)
20 | :response-format (ajax/json-response-format {:keywords? true})
21 | :on-success [on-success]
22 | :on-failure [on-failure]}
23 | data (assoc :params data))]
24 | {:http-xhrio xhrio
25 | :db db}))
26 |
27 | (def http-post (partial http :post))
28 | (def http-get (partial http :get))
29 |
30 |
--------------------------------------------------------------------------------
/src/cljs/frontend/main.cljs:
--------------------------------------------------------------------------------
1 | (ns frontend.main
2 | (:require [re-frame.core :as re-frame]
3 | [re-frame.db]
4 | [reagent.dom :as r-dom]
5 | [day8.re-frame.http-fx] ; Needed to register :http-xhrio to re-frame.
6 | [reagent-dev-tools.core :as dev-tools]
7 | [reitit.coercion.spec :as rss]
8 | [reitit.frontend :as rf]
9 | [reitit.frontend.controllers :as rfc]
10 | [reitit.frontend.easy :as rfe]
11 | [frontend.util :as f-util]
12 | [frontend.header :as f-header]
13 | [frontend.routes.index :as f-index]
14 | [frontend.state :as f-state]
15 | [frontend.routes.login :as f-login]
16 | [frontend.routes.product-groups :as f-product-group]
17 | [frontend.routes.products :as f-products]
18 | [frontend.routes.product :as f-product]
19 | ))
20 |
21 | ;; ******************************************************************
22 | ;; NOTE: When starting ClojureScript REPL, give first command:
23 | ; (shadow.cljs.devtools.api/repl :app)
24 | ; to connect the REPL to the app running in the browser.
25 | ;; ******************************************************************
26 |
27 | ;;; Events ;;;
28 |
29 |
30 | (re-frame/reg-event-db
31 | ::initialize-db
32 | (fn [_ _]
33 | {:current-route nil
34 | :token nil
35 | :debug true
36 | :login-status nil
37 | :username nil
38 | :product-groups nil
39 | :products nil
40 | :product nil
41 | }))
42 |
43 | (re-frame/reg-event-fx
44 | ::f-state/navigate
45 | (fn [_ [_ & route]]
46 | ;; See `navigate` effect in routes.cljs
47 | {::navigate! route}))
48 |
49 | (re-frame/reg-event-db
50 | ::f-state/navigated
51 | (fn [db [_ new-match]]
52 | (let [old-match (:current-route db)
53 | new-path (:path new-match)
54 | controllers (rfc/apply-controllers (:controllers old-match) new-match)]
55 | (js/console.log (str "new-path: " new-path))
56 | (cond-> (assoc db :current-route (assoc new-match :controllers controllers))
57 | (= "/" new-path) (-> (assoc :login-status nil)
58 | (assoc :user nil))))))
59 |
60 | (re-frame/reg-event-fx
61 | ::f-state/logout
62 | (fn [cofx [_]]
63 | (let [db (:db cofx)]
64 | {:db (-> db
65 | (assoc-in [:login] nil)
66 | (assoc-in [:username] nil)
67 | (assoc-in [:login-status] nil)
68 | (assoc-in [:token] nil))
69 | :fx [[:dispatch [::f-state/navigate ::f-state/login]]]})))
70 |
71 | #_(re-frame/reg-event-fx
72 | ::f-state/logout
73 | (fn [cofx [_]]
74 | {:db (assoc (:db cofx) :token nil)
75 | :fx [[:dispatch [::f-state/navigate ::f-state/home]]]}))
76 |
77 | ;;; Views ;;;
78 |
79 |
80 | (defn home-page []
81 | (let [token @(re-frame/subscribe [::f-state/token])]
82 | ; If we have jwt in app db we are logged-in.
83 | (f-util/clog "ENTER home-page")
84 | [f-index/landing-page]
85 | #_(if jwt
86 | (re-frame/dispatch [::f-state/navigate ::f-state/product-group])
87 | ;; NOTE: You need the div here or you are going to see only the debug-panel!
88 | [:div
89 | (welcome)
90 | (f-util/debug-panel {:token token})])))
91 |
92 |
93 | ;;; Effects ;;;
94 |
95 | ;; Triggering navigation from events.
96 | (re-frame/reg-fx
97 | ::navigate!
98 | (fn [route]
99 | (apply rfe/push-state route)))
100 |
101 | ;;; Routes ;;;
102 |
103 | (defn href
104 | "Return relative url for given route. Url can be used in HTML links."
105 | ([k]
106 | (href k nil nil))
107 | ([k params]
108 | (href k params nil))
109 | ([k params query]
110 | (rfe/href k params query)))
111 |
112 |
113 | (def routes-dev
114 | ["/"
115 | [""
116 | {:name ::f-state/home
117 | :view home-page
118 | :link-text "Home"
119 | :controllers
120 | [{:start (fn [& params] (js/console.log (str "Entering home page, params: " params)))
121 | :stop (fn [& params] (js/console.log (str "Leaving home page, params: " params)))}]}]
122 | ["login"
123 | {:name ::f-state/login
124 | :view f-login/login
125 | :link-text "Login"
126 | :controllers [{:start (fn [& params] (js/console.log (str "Entering login, params: " params)))
127 | :stop (fn [& params] (js/console.log (str "Leaving login, params: " params)))}]}]
128 | ["product-group"
129 | {:name ::f-state/product-groups
130 | :view f-product-group/product-groups
131 | :link-text "Product group"
132 | :controllers [{:start (fn [& params] (js/console.log (str "Entering product-group, params: " params)))
133 | :stop (fn [& params] (js/console.log (str "Leaving product-group, params: " params)))}]}]
134 | ["products/:pgid"
135 | {:name ::f-state/products
136 | :parameters {:path {:pgid int?}}
137 | :view f-products/products
138 | :link-text "Products"
139 | :controllers [{:start (fn [& params] (js/console.log (str "Entering products, params: " params)))
140 | :stop (fn [& params] (js/console.log (str "Leaving products, params: " params)))}]}]
141 | ["product/:pgid/:pid"
142 | {:name ::f-state/product
143 | :parameters {:path {:pgid int?
144 | :pid int?}}
145 | :view f-product/product
146 | :link-text "Product"
147 | :controllers [{:start (fn [& params] (js/console.log (str "Entering product, params: " params)))
148 | :stop (fn [& params] (js/console.log (str "Leaving product, params: " params)))}]}]])
149 |
150 | (def routes routes-dev)
151 |
152 | (defn on-navigate [new-match]
153 | (f-util/clog "on-navigate, new-match" new-match)
154 | (when new-match
155 | (re-frame/dispatch [::f-state/navigated new-match])))
156 |
157 | (def router
158 | (rf/router
159 | routes
160 | {:data {:coercion rss/coercion}}))
161 |
162 | (defn init-routes! []
163 | (js/console.log "initializing routes")
164 | (rfe/start!
165 | router
166 | on-navigate
167 | {:use-fragment true}))
168 |
169 | (defn router-component [_] ; {:keys [router] :as params}
170 | (f-util/clog "ENTER router-component")
171 | (let [current-route @(re-frame/subscribe [::f-state/current-route])
172 | path-params (:path-params current-route)
173 | _ (f-util/clog "router-component, path-params" path-params)]
174 | [:div
175 | [f-header/header]
176 | ; NOTE: when you supply the current-route to the view it can parse path-params there (from path)
177 | (when current-route
178 | [(-> current-route :data :view) current-route])]))
179 |
180 | ;;; Setup ;;;
181 |
182 | (def debug? ^boolean goog.DEBUG)
183 |
184 | (defn dev-setup []
185 | (when debug?
186 | (enable-console-print!)
187 | (println "dev mode")))
188 |
189 |
190 | (defn ^:dev/after-load start []
191 | (js/console.log "ENTER start")
192 | (re-frame/clear-subscription-cache!)
193 | (init-routes!)
194 | (r-dom/render [router-component {:router router}
195 | ;; TODO.
196 | ;; (if (:open? @dev-tools/dev-state)
197 | ;; {:style {:padding-bottom (str (:height @dev-tools/dev-state) "px")}})
198 | ]
199 | (.getElementById js/document "root")))
200 |
201 |
202 | (defn ^:export init []
203 | (js/console.log "ENTER init")
204 | (re-frame/dispatch-sync [::initialize-db])
205 | (dev-tools/start! {:state-atom re-frame.db/app-db})
206 | (dev-setup)
207 | (start))
208 |
209 | (comment
210 | ; With Calva, no need for this:
211 | ;(shadow.cljs.devtools.api/repl :app)
212 | ; But you have to start the browser since the browser is the runtime for the repl!
213 | ; http://localhost:7171
214 | (+ 1 2)
215 | ;(reagent.dom/render [])
216 | ;(require '[hashp.core :include-macros true])
217 | ;(let [a #p (range 5)] a)
218 | )
219 |
220 |
221 |
--------------------------------------------------------------------------------
/src/cljs/frontend/routes/index.cljs:
--------------------------------------------------------------------------------
1 | (ns frontend.routes.index
2 | (:require [re-frame.core :as re-frame]
3 | [frontend.state :as f-state]
4 | ))
5 |
6 | (defn landing-page []
7 | [:div.app
8 | [:div
9 | [:div.p-4
10 | [:p.text-left.p-4
11 | "This is a demo webstore built for learning the following technologies:"]
12 | [:div.p-4
13 | [:p.text-left.font-bold "Backend:"]
14 | [:ul.list-disc.list-inside
15 | [:li "Clojure"]
16 | [:li "JVM"]]]
17 | [:div.p-4
18 | [:p.text-left.font-bold "Frontend:"]
19 | [:ul.list-disc.list-inside
20 | [:li "Clojurescript"]
21 | [:li "React with Reagent"]
22 | [:li "Re-frame"]
23 | [:li "Tailwind"]]]
24 | [:div.p-4.text-center
25 | [:button
26 | {:on-click (fn [e]
27 | (.preventDefault e)
28 | (re-frame/dispatch [::f-state/navigate ::f-state/product-groups]))}
29 | "Enter"]]]]])
--------------------------------------------------------------------------------
/src/cljs/frontend/routes/login.cljs:
--------------------------------------------------------------------------------
1 | (ns frontend.routes.login
2 | (:require [reagent.core :as r]
3 | [re-frame.core :as re-frame]
4 | [frontend.http :as f-http]
5 | [frontend.state :as f-state]
6 | [frontend.util :as f-util]))
7 |
8 | (defn empty-creds []
9 | {:username "" :password ""})
10 |
11 |
12 | (re-frame/reg-event-db
13 | ::login-ret-ok
14 | (fn [db [_ res-body]]
15 | (-> db
16 | (assoc-in [:login :response] {:ret :ok :msg (:msg res-body)})
17 | (assoc-in [:token] (:token res-body))
18 | (assoc-in [:login-status] :logged-in))))
19 |
20 |
21 | (re-frame/reg-event-db
22 | ::login-ret-failed
23 | (fn [db [_ res-body]]
24 | (f-util/clog "reg-event-db failed" db)
25 | (assoc-in db [:login :response] {:ret :failed
26 | :msg (get-in res-body [:response :msg])})))
27 |
28 | (re-frame/reg-event-db
29 | ::save-username
30 | (fn [db [_ username]]
31 | (-> db
32 | (assoc-in [:username] username))))
33 |
34 |
35 | (re-frame/reg-sub
36 | ::login-response
37 | (fn [db]
38 | (f-util/clog "reg-sub" db)
39 | (:response (:login db))))
40 |
41 |
42 | (re-frame/reg-event-fx
43 | ::login-user
44 | (fn [{:keys [db]} [_ user-data]]
45 | (f-util/clog "login-user, user-data" user-data)
46 | (f-http/http-post db "/api/login" user-data ::login-ret-ok ::login-ret-failed)))
47 |
48 |
49 | (defn login []
50 | (let [login-data (r/atom (empty-creds))]
51 | (fn []
52 | ; NOTE: The re-frame subscription needs to be inside the rendering function or the watch
53 | ; is not registered to the rendering function.
54 | (let [_ (f-util/clog "ENTER login")
55 | title "You need to login to use the web store"
56 | {:keys [ret _msg] :as _r-body} @(re-frame/subscribe [::login-response])
57 | _ (when (= ret :ok) (re-frame/dispatch [::f-state/navigate ::f-state/product-groups]))
58 | #_#__ (f-util/clog "********************** ret" ret)]
59 | [:div.app
60 | [:div.p-4
61 | [:p.text-left.text-lg.font-bold.p-4 title]
62 | (when (= ret :failed)
63 | [:div {:className "flex grow w-3/4 p-4"}
64 | (re-frame/dispatch [::save-username nil])
65 | [f-util/error-message "Login failed!" "Username or password is wrong."]])]
66 | [:div.flex.grow.justify-center.items-center
67 | [:div {:className "flex grow w-1/2 p-4"}
68 | [:form
69 | [:div.mt-3
70 | [f-util/input "Username" :username "text" login-data]
71 | [f-util/input "Password" :password "password" login-data]
72 | [:div.flex.flex-col.justify-center.items-center.mt-5
73 | [:button {:className "login-button"
74 | :on-click (fn [e]
75 | (.preventDefault e)
76 | (re-frame/dispatch [::login-user @login-data])
77 | (re-frame/dispatch [::save-username (:username @login-data)])
78 | )}
79 | "Login"]]]]]]]))))
80 |
81 |
--------------------------------------------------------------------------------
/src/cljs/frontend/routes/product.cljs:
--------------------------------------------------------------------------------
1 | (ns frontend.routes.product
2 | (:require
3 | [re-frame.core :as re-frame]
4 | [day8.re-frame.http-fx]
5 | [frontend.http :as f-http]
6 | [frontend.state :as f-state]
7 | [frontend.util :as f-util]))
8 |
9 |
10 | (re-frame/reg-event-db
11 | ::ret-ok
12 | (fn [db [_ res-body]]
13 | (f-util/clog "reg-event-db ok: " res-body)
14 | (let [pgid (:pg-id res-body)
15 | pid (:p-id res-body)]
16 | (-> db
17 | (assoc-in [:product :response] {:ret :ok :res-body res-body})
18 | (assoc-in [:product :data] (:product res-body))))))
19 |
20 |
21 | (re-frame/reg-event-db
22 | ::ret-failed
23 | (fn [db [_ res-body]]
24 | (f-util/clog "reg-event-db failed" db)
25 | (assoc-in db [:product :response] {:ret :failed
26 | :msg (get-in res-body [:response :msg])})))
27 |
28 |
29 | (re-frame/reg-sub
30 | ::product-data
31 | (fn [db params]
32 | (f-util/clog "::product-data, params" params)
33 | (let [data (get-in db [:product :data]) ]
34 | data)))
35 |
36 |
37 | (re-frame/reg-event-fx
38 | ::get-product
39 | (fn [{:keys [db]} [_ pg-id p-id]]
40 | (f-util/clog "get-product, pg-id, p-id" {:pg-id pg-id :p-id p-id})
41 | (f-http/http-get db (str "/api/product/" pg-id "/" p-id) nil ::ret-ok ::ret-failed)))
42 |
43 |
44 | (defn product-table [data]
45 | [:table
46 | [:thead
47 | [:tr
48 | [:th "Field"]
49 | [:th "Value"]]]
50 | [:tbody
51 | (map (fn [item]
52 | (let [[field value] item]
53 | [:tr {:key field}
54 | [:td field]
55 | [:td value]]))
56 | data)]])
57 |
58 |
59 | (defn product
60 | "Product view."
61 | [match]
62 | (let [_ (f-util/clog "ENTER product-page, match" match)
63 | {:keys [path]} (:parameters match)
64 | {:keys [pgid pid]} path
65 | pgid (str pgid)
66 | pid (str pid)
67 | _ (f-util/clog "path" path)
68 | _ (f-util/clog "pgid" pgid)
69 | _ (f-util/clog "pid" pid)]
70 |
71 | (fn []
72 | (let [title "Product"
73 | login-status @(re-frame/subscribe [::f-state/login-status])
74 | token @(re-frame/subscribe [::f-state/token])
75 | _ (when-not (and login-status token) (re-frame/dispatch [::f-state/navigate ::f-state/login]))
76 | data @(re-frame/subscribe [::product-data pgid pid])
77 | _ (when-not data (re-frame/dispatch [::get-product pgid pid]))]
78 | [:div.app
79 | [:div.p-4
80 | [:p.text-left.text-lg.font-bold.p-4 title]
81 | [:div.p-4
82 | [product-table data]]]]))))
83 |
84 |
--------------------------------------------------------------------------------
/src/cljs/frontend/routes/product_groups.cljs:
--------------------------------------------------------------------------------
1 | (ns frontend.routes.product-groups
2 | (:require [re-frame.core :as re-frame]
3 | [reitit.frontend.easy :as rfe]
4 | ["@tanstack/react-table" :as rt]
5 | ["react" :as react :default useMemo]
6 | [frontend.state :as f-state]
7 | [frontend.http :as f-http]
8 | [frontend.util :as f-util]
9 | [reagent.core :as r]))
10 |
11 | (re-frame/reg-event-db
12 | ::ret-ok
13 | (fn [db [_ res-body]]
14 | (f-util/clog "reg-event-db ok: " res-body)
15 | (-> db
16 | (assoc-in [:product-groups :response] {:ret :ok :res-body res-body})
17 | (assoc-in [:product-groups :data] (:product-groups res-body)))))
18 |
19 | (re-frame/reg-event-db
20 | ::ret-failed
21 | (fn [db [_ res-body]]
22 | (f-util/clog "reg-event-db failed" db)
23 | (assoc-in db [:product-groups :response] {:ret :failed
24 | :msg (get-in res-body [:response :msg])})))
25 |
26 | (re-frame/reg-sub
27 | ::product-groups-data
28 | (fn [db]
29 | (get-in db [:product-groups :data])))
30 |
31 |
32 | (re-frame/reg-event-fx
33 | ::get-product-groups
34 | (fn [{:keys [db]} [_]]
35 | (f-util/clog "get-product-groups")
36 | (f-http/http-get db "/api/product-groups" nil ::ret-ok ::ret-failed)))
37 |
38 |
39 | ;; Let's implement a simple basic html table first,
40 | ;; and later provide an example using @tanstack/react-table.
41 | (defn product-groups-simple-table
42 | [data]
43 | (let [_ (f-util/clog "ENTER product-groups-table")]
44 | [:div.p-4
45 | [:table
46 | [:thead
47 | [:tr
48 | [:th "Id"]
49 | [:th "Name"]]]
50 | [:tbody
51 | (map (fn [item]
52 | (let [{pg-id :pgId pg-name :name} item]
53 | [:tr {:key pg-id}
54 | [:td [:a {:href (rfe/href ::f-state/products {:pgid pg-id})} pg-id]]
55 | [:td pg-name]]))
56 | data)]]]))
57 |
58 |
59 | (defn mylink
60 | [pg-id]
61 | [:<>
62 | [:a {:href (rfe/href ::f-state/products {:pgid pg-id})} pg-id]])
63 |
64 | ;; Example of Clojurescript / Javascript interop.
65 | ;; Compare to equivalent JSX implementation:
66 | ;; https://github.com/karimarttila/js-node-ts-react/blob/main/frontend/src/routes/product_groups.tsx#L36
67 | (defn product-groups-react-table
68 | [data]
69 | (let [_ (f-util/clog "ENTER product-groups-table")
70 | columnHelper (rt/createColumnHelper)
71 | columns #js [(.accessor columnHelper "pgId" #js {:header "Id" :cell (fn [info] (reagent.core/as-element [mylink (.getValue info)]))})
72 | (.accessor columnHelper "name" #js {:header "Name" :cell (fn [info] (.getValue info))})]
73 | table (rt/useReactTable #js {:columns columns :data (clj->js data) :getCoreRowModel (rt/getCoreRowModel)})
74 | ^js headerGroups (.getHeaderGroups table)]
75 | [:div.p-4
76 | [:table
77 | [:thead
78 | (for [^js headerGroup headerGroups]
79 | [:tr {:key (.-id headerGroup) }
80 | (for [^js header (.-headers headerGroup)]
81 | [:th {:key (.-id header) }
82 | (if (.-isPlaceholder header)
83 | nil
84 | (rt/flexRender (.. header -column -columnDef -header) (.getContext header)))])])]
85 | [:tbody
86 | (for [^js row (.-rows (.getRowModel table))]
87 | [:tr {:key (.-id row)}
88 | (for [^js cell (.getVisibleCells row)]
89 | [:td {:key (.-id cell)}
90 | (rt/flexRender (.. cell -column -columnDef -cell) (.getContext cell))])])]]]))
91 |
92 |
93 | (defn product-groups []
94 | (let [_ (f-util/clog "ENTER product-groups")]
95 | (fn []
96 | (let [title " Product Groups"
97 | login-status @(re-frame/subscribe [::f-state/login-status])
98 | token @(re-frame/subscribe [::f-state/token])
99 | _ (when-not (and login-status token) (re-frame/dispatch [::f-state/navigate ::f-state/login]))
100 | product-groups-data @(re-frame/subscribe [::product-groups-data])
101 | _ (when-not product-groups-data (re-frame/dispatch [::get-product-groups]))
102 | ]
103 | [:div.app
104 | [:div.p-4
105 | [:p.text-left.text-lg.font-bold.p-4 title]
106 | [:div.p-4
107 | [product-groups-simple-table product-groups-data]
108 | ;[:f> product-groups-react-table product-groups-data]
109 | ]]]))))
110 |
111 |
--------------------------------------------------------------------------------
/src/cljs/frontend/routes/products.cljs:
--------------------------------------------------------------------------------
1 | (ns frontend.routes.products
2 | (:require
3 | [re-frame.core :as re-frame]
4 | [reitit.frontend.easy :as rfe]
5 | [day8.re-frame.http-fx]
6 | [frontend.http :as f-http]
7 | [frontend.state :as f-state]
8 | [frontend.util :as f-util]))
9 |
10 |
11 | (re-frame/reg-event-db
12 | ::ret-ok
13 | (fn [db [_ res-body]]
14 | (f-util/clog "reg-event-db")
15 | (let [pg-id (:pgId res-body)]
16 | (-> db
17 | (assoc-in [:products :response] {:ret :ok :res-body res-body})
18 | (assoc-in [:products :data pg-id] (:products res-body))))))
19 |
20 | (re-frame/reg-event-db
21 | ::ret-failed
22 | (fn [db [_ res-body]]
23 | (f-util/clog "reg-event-db failed" db)
24 | (assoc-in db [:products :response] {:ret :failed
25 | :msg (get-in res-body [:response :msg])})))
26 |
27 | (re-frame/reg-sub
28 | ::products-data
29 | (fn [db params]
30 | (f-util/clog "::products-data")
31 | (f-util/clog "params: " params)
32 | (let [myParseInt (fn [s] (js/parseInt s 10))
33 | pgid (f-util/myParseInt (second params))
34 | data (get-in db [:products :data])]
35 | (get-in data [pgid]))))
36 |
37 |
38 |
39 | (re-frame/reg-sub
40 | ::product-group-name
41 | (fn [db params]
42 | (f-util/clog "::product-group-name, params" params)
43 | (let [pg-id (f-util/myParseInt (second params))]
44 | (:name (first (filter (fn [item]
45 | (= pg-id (:pgId item)))
46 | (get-in db [:product-groups :data])))))))
47 |
48 |
49 | ;; We could just get the specific product from re-frame :products key,
50 | ;; but let's fetch it using the backend API.
51 | (re-frame/reg-event-fx
52 | ::get-products
53 | (fn [{:keys [db]} [_ pg-id]]
54 | (f-util/clog "get-product, pg-id" pg-id)
55 | (f-http/http-get db (str "/api/products/" pg-id) nil ::ret-ok ::ret-failed)))
56 |
57 |
58 | (re-frame/reg-event-db
59 | ::reset-product
60 | (fn [db param]
61 | (f-util/clog "reg-event-db reset-product" param)
62 | (assoc-in db [:product] nil)))
63 |
64 | (defn products-table [data]
65 | [:table
66 | [:thead
67 | [:tr
68 | [:th "Id"]
69 | [:th "Name"]]]
70 | [:tbody
71 | (map (fn [item]
72 | (let [{pg-id :pgId p-id :pId title :title} item]
73 | [:tr {:key p-id}
74 | [:td [:a {:href (rfe/href ::f-state/product {:pgid pg-id :pid p-id})} p-id]]
75 | [:td title]]))
76 | data)]])
77 |
78 |
79 | (defn products
80 | "Products view."
81 | [match] ; NOTE: This is the current-route given as parameter to the view. You can get the pgid also from :path-params.
82 | (let [_ (f-util/clog "ENTER products-page, match" match)
83 | {:keys [path]} (:parameters match)
84 | {:keys [pgid]} path
85 | pgid (str pgid)
86 | _ (f-util/clog "path" path)
87 | _ (f-util/clog "pgid" pgid)]
88 | (fn []
89 | (let [;; Reset product so that we are forced to fetch the new product in the product page.
90 | _ (re-frame/dispatch [::reset-product])
91 | login-status @(re-frame/subscribe [::f-state/login-status])
92 | token @(re-frame/subscribe [::f-state/token])
93 | _ (when-not (and login-status token) (re-frame/dispatch [::f-state/navigate ::f-state/login]))
94 | data @(re-frame/subscribe [::products-data pgid])
95 | product-group-name @(re-frame/subscribe [::product-group-name pgid])
96 | title (str "Products - " product-group-name)
97 | _ (when-not data (re-frame/dispatch [::get-products pgid]))]
98 | [:div.app
99 | [:div.p-4
100 | [:p.text-left.text-lg.font-bold.p-4 title]
101 | [:div.p-4
102 | [products-table data]]]]))))
103 |
104 |
105 |
--------------------------------------------------------------------------------
/src/cljs/frontend/state.cljs:
--------------------------------------------------------------------------------
1 | (ns frontend.state
2 | (:require [re-frame.core :as re-frame]))
3 |
4 | ;; Subscriptions
5 |
6 | (re-frame/reg-sub
7 | ::current-route
8 | (fn [db]
9 | (:current-route db)))
10 |
11 | (re-frame/reg-sub
12 | ::token
13 | (fn [db]
14 | (:token db)))
15 |
16 | (re-frame/reg-sub
17 | ::username
18 | (fn [db]
19 | (:username db)))
20 |
21 | (re-frame/reg-sub
22 | ::login-status
23 | (fn [db]
24 | (:login-status db)))
25 |
26 | (re-frame/reg-sub
27 | ::debug
28 | (fn [db]
29 | (:debug db)))
30 |
--------------------------------------------------------------------------------
/src/cljs/frontend/util.cljs:
--------------------------------------------------------------------------------
1 | (ns frontend.util
2 | (:require [re-frame.core :as re-frame]
3 | [cljs.pprint]
4 | ;[clojure.pprint]
5 | [frontend.state :as sf-state]))
6 |
7 |
8 | ;; Application wide properties.
9 | (def backend-host-config {:host "localhost" :port 7171})
10 |
11 |
12 | (defn valid?
13 | "Simple validator. Checks in [k v] v is a string and not empty."
14 | [[_ v]]
15 | (and (string? v) (seq v)))
16 |
17 |
18 | (defn save!
19 | "Fetches an event value and swaps the value of given atom a's key k to this value."
20 | [a k]
21 | #(swap! a assoc k (-> % .-target .-value)))
22 |
23 |
24 | (defn input
25 | "Input field component for e.g. First name, Last name, Email address and Password."
26 | [label k type state]
27 | [:div.flex.flex-wrap.gap-2.items-center.mt-1
28 | [:label {:htmlFor (name k) :className "login-label"} label]
29 | [:div
30 | [:input {:type "text"
31 | :id (name k)
32 | :name (name k)
33 | :className "login-input"
34 | :placeholder (name k)
35 | :value (k @state)
36 | :on-change (save! state k)
37 | :required true}]]])
38 |
39 |
40 | (defn get-base-url
41 | []
42 | (let [host (:host backend-host-config)
43 | port (:port backend-host-config)
44 | url (str "http://" host ":" port)]
45 | url))
46 |
47 |
48 | (defn debug-panel
49 | "Debug panel - you can use this panel in any view to show some page specific debug data."
50 | [data]
51 | (let [debug @(re-frame/subscribe [::sf-state/debug])]
52 | #_(js/console.log (str "ENTER debug-panel, debug: " debug))
53 | (if debug
54 | [:div.sf-debug-panel
55 | [:hr.sf-debug-panel.hr]
56 | [:h3.sf-debug-panel.header "DEBUG-PANEL"]
57 | [:pre.sf-debug-panel.body (with-out-str (cljs.pprint/pprint data))]])))
58 |
59 |
60 | (defn clog
61 | "Javascript console logger helper."
62 | ([msg] (clog msg nil))
63 | ([msg data]
64 | (let [buf (if data
65 | (str msg ": " data)
66 | msg)]
67 | (js/console.log buf))))
68 |
69 |
70 | (defn error-message
71 | [title msg]
72 | [:<>
73 | [:div.bg-red-100.border.border-red-400.text-red-700.px-4.py-3.rounded.relative
74 | {:role "alert"}
75 | [:strong.font-bold.mr-2 title]
76 | [:span.block.sm:inline msg]]])
77 |
78 |
79 | (defn myParseInt
80 | [s]
81 | (js/parseInt s 10))
--------------------------------------------------------------------------------
/src/css/app.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html {
6 | font-family: sans-serif;
7 | font-size: 14px;
8 | @apply h-screen bg-gradient-to-b from-gray-100 to-gray-300
9 | }
10 |
11 | table {
12 | border: 1px solid lightgray;
13 | }
14 |
15 | tbody {
16 | border-bottom: 1px solid lightgray;
17 | }
18 |
19 | th {
20 | border-bottom: 1px solid lightgray;
21 | border-right: 1px solid lightgray;
22 | padding: 2px 4px;
23 | }
24 |
25 |
26 | a {
27 | @apply underline text-blue-600 hover:text-blue-800 visited:text-purple-600 cursor-pointer
28 | }
29 |
30 | button {
31 | @apply bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded
32 | }
33 |
34 | label {
35 | @apply font-medium text-gray-700;
36 | }
37 |
38 | .login-input {
39 | @apply bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500;
40 | }
41 |
42 | .login-label {
43 | @apply block mb-2 text-sm font-medium text-gray-900 dark:text-white;
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | "./src/**/*.cljs"
4 | ],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [],
9 | }
10 |
--------------------------------------------------------------------------------
/test/clj/backend/db/domain_test.clj:
--------------------------------------------------------------------------------
1 | (ns backend.db.domain-test
2 | (:require [clojure.test :refer [deftest use-fixtures is testing]]
3 | [clojure.tools.logging :as log]
4 | [backend.db.domain :as b-domain]
5 | [backend.test-config :as test-config]))
6 |
7 | (defn init-fixture []
8 | ;; Do nothing.
9 | )
10 |
11 |
12 | (defn domain-test-fixture
13 | [f]
14 | (log/debug "ENTER domain-test-fixture")
15 | (test-config/test-system-fixture-runner init-fixture f)
16 | (log/debug "EXIT domain-test-fixture"))
17 |
18 |
19 | (use-fixtures :each domain-test-fixture)
20 |
21 |
22 | ;; Experimental testing.
23 | ;; (def my-atom (atom nil))
24 |
25 |
26 | (deftest get-domain-data-test
27 | (log/debug "ENTER get-domain-data-test")
28 | (testing "Testing get domain data"
29 | (let [domain-data (b-domain/get-domain-data (-> (test-config/test-config) :backend/env :data-dir))
30 | ;;_ (reset! my-atom domain-data)
31 | ]
32 | (testing "Testing product groups"
33 | (let [product-groups (:product-groups domain-data)
34 | product-groups-len (count product-groups)
35 | right-vec [{:pgId 1, :name "Books"} {:pgId 2, :name "Movies"}]]
36 | (is (= 2 product-groups-len))
37 | (is (= right-vec product-groups))))
38 | (testing "Testing products"
39 | (let [products (:products domain-data)
40 | products-len (count products)]
41 | (is (= 204 products-len))))
42 | (testing "Testing specific product"
43 | (let [products (:products domain-data)
44 | product (b-domain/get-product 2 49 products)
45 | right-product {:pId 49,
46 | :pgId 2,
47 | :title "Once Upon a Time in the West",
48 | :price 14.4,
49 | :director "Leone, Sergio",
50 | :year 1968,
51 | :country "Italy-USA",
52 | :genre "Western"}]
53 | (is (= right-product product))))))
54 | (testing "Using the domain data in the internal test system"
55 | (let [domain-data (:domain @(-> @test-config/test-system :backend/env :db))]
56 | (testing "Testing product groups"
57 | (let [product-groups (:product-groups domain-data)
58 | product-groups-len (count product-groups)
59 | right-vec [{:pgId 1, :name "Books"} {:pgId 2, :name "Movies"}]]
60 | (is (= 2 product-groups-len))
61 | (is (= right-vec product-groups))))
62 | (testing "Testing products"
63 | (let [products (:products domain-data)
64 | products-len (count products)]
65 | (is (= 204 products-len))))
66 | (testing "Testing products of a specific product group"
67 | (let [products (:products domain-data)
68 | products-1 (b-domain/get-products 1 products)
69 | products-2 (b-domain/get-products 2 products)]
70 | (is (= 35 (count products-1)))
71 | (is (= 169 (count products-2)))
72 | (is (= 204 (+ (count products-1) (count products-2))))))
73 | (testing "Testing specific product"
74 | (let [products (:products domain-data)
75 | product (b-domain/get-product 2 49 products)
76 | right-product {:pId 49,
77 | :pgId 2,
78 | :title "Once Upon a Time in the West",
79 | :price 14.4,
80 | :director "Leone, Sergio",
81 | :year 1968,
82 | :country "Italy-USA",
83 | :genre "Western"}]
84 | (is (= right-product product))))))
85 |
86 | )
87 |
88 |
89 | (comment
90 | (test-config/test-config)
91 | (-> (test-config/test-config) :backend/env :data-dir)
92 | ;; @my-atom
93 | ;; (keys @my-atom)
94 | ;; (:product-groups @my-atom)
95 | ;; (:products @my-atom)
96 | ;; (count (:products @my-atom))
97 |
98 | )
--------------------------------------------------------------------------------
/test/clj/backend/db/users_test.clj:
--------------------------------------------------------------------------------
1 | (ns backend.db.users-test
2 | (:require [clojure.test :refer [deftest use-fixtures is testing]]
3 | [clojure.tools.logging :as log]
4 | [backend.db.users :as b-users]
5 | [backend.test-config :as test-config]))
6 |
7 | (defn init-fixture []
8 | ;; Do nothing.
9 | )
10 |
11 |
12 | (defn domain-test-fixture
13 | [f]
14 | (log/debug "ENTER domain-test-fixture")
15 | (test-config/test-system-fixture-runner init-fixture f)
16 | (log/debug "EXIT domain-test-fixture"))
17 |
18 |
19 | (use-fixtures :each domain-test-fixture)
20 |
21 |
22 | ;; Experimental testing.
23 | ;; (def my-atom (atom nil))
24 |
25 |
26 | (deftest get-users-test
27 | (log/debug "ENTER get-users-test")
28 | (testing "Testing users"
29 | (let [env (:backend/env @test-config/test-system)]
30 | (b-users/clear-sessions env)
31 | (is (= (count (b-users/get-sessions env)) 0))
32 | (let [token-jartsa (b-users/validate-user env "jartsa" "joo")]
33 | (is (not (nil? token-jartsa)))
34 | (let [decoded-token (b-users/validate-token env token-jartsa)]
35 | (is (not (nil? token-jartsa)))
36 | (is (= "jartsa" (:username decoded-token)))
37 | (let [sessions (b-users/get-sessions env)
38 | session (first sessions)]
39 | (is (= 1 (count sessions)))
40 | (is (= "jartsa" (:username session)))
41 | (is (= "joo" (:password session)))
42 | (is (= token-jartsa (:token session)))))
43 | (let [_token-rane (b-users/validate-user env "rane" "jee")]
44 | (let [sessions (b-users/get-sessions env)]
45 | (is (= 2 (count sessions)))))
46 | ;; Create a new session for jartsa: the old one should be removed
47 | (let [token-jartsa2 (b-users/validate-user env "jartsa" "joo")]
48 | (is (not (nil? token-jartsa2)))
49 | (let [sessions (b-users/get-sessions env)]
50 | (is (= 2 (count sessions)))))))))
51 |
52 | (deftest user-validation-fails-test
53 | (log/debug "ENTER user-validation-fails-test")
54 | (testing "User validation fails"
55 | (let [env (:backend/env @test-config/test-system)]
56 | (b-users/clear-sessions env)
57 | (is (= 0 (count (b-users/get-sessions env))))
58 | (let [token-jartsa (b-users/validate-user env "jartsa" "WRONG")]
59 | (is (nil? token-jartsa))
60 | (let [sessions (b-users/get-sessions env)]
61 | (is (= 0 (count sessions))))))))
--------------------------------------------------------------------------------
/test/clj/backend/test_config.clj:
--------------------------------------------------------------------------------
1 | (ns backend.test-config
2 | (:require
3 | [clojure.tools.logging :as log]
4 | [clojure.pprint]
5 | [integrant.core :as ig]
6 | [clj-http.client :as http-client]
7 | [backend.core :as core])
8 | (:import (java.net ServerSocket)))
9 |
10 | (defonce test-system (atom nil))
11 |
12 | (defn random-port []
13 | (with-open [s (ServerSocket. 0)]
14 | (.getLocalPort s)))
15 |
16 | (defn test-config []
17 | (let [test-port (random-port)
18 | ; Overriding the port with random port, see TODO below.
19 | _ (log/debug (str "test-config, using web-server test port: " test-port))]
20 | (-> (core/system-config :test)
21 | ;; Use the same data dir also for test system. It just initializes data.
22 | (assoc-in [:backend/env :data-dir] "resources/data")
23 | (assoc-in [:backend/jetty :port] test-port)
24 | (assoc-in [:backend/nrepl :bind] nil)
25 | (assoc-in [:backend/nrepl :port] nil)
26 | (assoc-in [:backend/env :port] nil))))
27 |
28 |
29 | ;; (defmethod ig/init-key :backend/env [_ {:keys [_profile data-dir]}]
30 | ;; (log/debug "ENTER ig/init-key :backend/env")
31 | ;; (atom {:domain (b-domain/get-domain-data data-dir)
32 | ;; :sessions #{}
33 | ;; :users (create-users)}))
34 |
35 |
36 | ;; TODO: For some reason Integrant does not always run halt-key for webserver, really weird.
37 | ;; And you lose the reference to the web server and web server keeps the port binded => you have to restart REPL.
38 | #_(defn test-config-orig []
39 | (let [test-port (get-in (ss-config/create-config) [:web-server :test-server-port])]
40 | (-> (core/system-config)
41 | ; Overriding the port with test-port.
42 | (assoc-in [::core/web-server :port] test-port))))
43 |
44 | (defn halt []
45 | (swap! test-system #(if % (ig/halt! %))))
46 |
47 | (defn go []
48 | (halt)
49 | (reset! test-system (ig/init (test-config))))
50 |
51 |
52 | (defn test-system-fixture-runner [init-test-data, testfunc]
53 | (try
54 | (go)
55 | (init-test-data)
56 | (testfunc)
57 | (finally
58 | (halt))))
59 |
60 | (defn call-api [verb path headers body]
61 | (let [my-port (-> @test-system :backend/jetty .getConnectors first .getPort)
62 | my-fn (cond
63 | (= verb :get) http-client/get
64 | (= verb :post) http-client/post)]
65 | (try
66 | (select-keys
67 | (my-fn (str "http://localhost:" my-port "/api/" path)
68 | {:as :json
69 | :form-params body
70 | :headers headers
71 | :content-type :json
72 | :throw-exceptions false
73 | :coerce :always}) [:status :body])
74 | (catch Exception e
75 | (log/error (str "ERROR in -call-api: " (.getMessage e)))))))
76 |
77 |
78 | ; Rich comment.
79 | #_(comment
80 | (user/env)
81 | (test-config)
82 | *ns*
83 | (backend.test-config/-call-api :get "info" nil nil)
84 |
85 | (backend.test-config/go)
86 | (backend.test-config/halt)
87 | (backend.test-config/test-config)
88 | backend.test-config/test-system
89 | ;(backend.test-config/test-env)
90 | (backend.test-config/test-service)
91 |
92 | (backend.test-config/go)
93 |
94 |
95 | (:domain @(-> @test-system :backend/env :db))
96 |
97 | (backend.test-config/halt)
98 | (->> (:out (clojure.java.shell/sh "netstat" "-an")) (clojure.string/split-lines) (filter #(re-find #".*:::61.*LISTEN.*" %)))
99 | )
100 |
--------------------------------------------------------------------------------
/test/clj/backend/webserver_test.clj:
--------------------------------------------------------------------------------
1 | (ns backend.webserver-test
2 | (:require [clojure.test :refer [deftest use-fixtures is testing]]
3 | [clojure.tools.logging :as log]
4 | [clojure.data.codec.base64 :as base64]
5 | [backend.db.users :as b-users]
6 | [backend.test-config :as test-config]))
7 |
8 |
9 | (defn init-fixture
10 | []
11 | (log/debug "ENTER init-fixture")
12 | (b-users/clear-sessions (:backend/env @test-config/test-system)))
13 |
14 | (defn webserver-test-fixture
15 | [f]
16 | (log/debug "ENTER webserver-test-fixture")
17 | (test-config/test-system-fixture-runner init-fixture f)
18 | (log/debug "EXIT webserver-test-fixture"))
19 |
20 | (use-fixtures :each webserver-test-fixture)
21 |
22 | (deftest info-test
23 | (log/debug "ENTER info-test")
24 | (testing "GET: /api/info"
25 | (let [ret (test-config/call-api :get "info" nil nil)]
26 | (is (= 200 (ret :status) 200))
27 | (is (= {:info "/info.html => Info in HTML format"} (ret :body))))))
28 |
29 | (deftest ping-get-test
30 | (log/debug "ENTER ping-get-test")
31 | (testing "GET: /api/ping"
32 | (let [ret (test-config/call-api :get "ping" nil nil)]
33 | (is (= 200 (ret :status)))
34 | (let [body (ret :body)]
35 | (is (= "pong" (:reply body)))
36 | (is (= "ok" (:ret body)))))))
37 |
38 | (deftest failed-ping-get-extra-query-params-test
39 | (log/debug "ENTER failed-ping-get-extra-query-params-test")
40 | (testing "GET: /api/ping"
41 | (let [ret (test-config/call-api :get "ping?a=1" nil nil)]
42 | (is (= 400 (ret :status) 400))
43 | (is (= {:coercion "malli"
44 | :humanized {:a ["disallowed key"]}
45 | :in ["request"
46 | "query-params"]
47 | :type "reitit.coercion/request-coercion"}
48 | (ret :body))))))
49 |
50 | (deftest ping-post-test
51 | (log/debug "ENTER ping-post-test")
52 | (testing "POST: /api/ping"
53 | (let [ret (test-config/call-api :post "ping" nil {:ping "hello"})]
54 | (is (= 200 (ret :status)))
55 | (is (= {:reply "pong" :request "hello" :ret "ok"} (ret :body) )))))
56 |
57 | (deftest failed-ping-post-missing-key-test
58 | (log/debug "ENTER failed-ping-post-missing-key-test")
59 | (testing "POST: /api/ping"
60 | (let [ret (test-config/call-api :post "ping" nil {:wrong-key "hello"})]
61 | (is (= 400 (ret :status) 400))
62 | (is (= {:coercion "malli"
63 | :humanized {:ping ["missing required key"]}
64 | :in ["request"
65 | "body-params"]
66 | :type "reitit.coercion/request-coercion"}
67 | (ret :body))))))
68 |
69 | (deftest login-test
70 | (log/debug "ENTER login-test")
71 | (testing "POST: /api/login"
72 | ; First with good credentials.
73 | (let [good-test-body {:username "jartsa" :password "joo"}
74 | bad-test-body {:username "jartsa" :password "WRONG-PASSWORD"}]
75 | (let [ret (test-config/call-api :post "login" nil good-test-body)]
76 | (is (= 200 (ret :status)))
77 | (is (= "ok" (get-in ret [:body :ret]) "ok"))
78 | (is (= java.lang.String (type (get-in ret [:body :token]))))
79 | (is (< 30 (count (get-in ret [:body :token])))))
80 | ; Next with bad credentials.
81 | (let [ret (test-config/call-api :post "login" nil bad-test-body)]
82 | (is (= 400 (ret :status)))
83 | (is (= "failed" (get-in ret [:body :ret])))
84 | (is (= "Login failed" (get-in ret [:body :msg])))))))
85 |
86 | (deftest product-groups-test
87 | (log/debug "ENTER product-groups-test")
88 | (testing "GET: /api/product-groups"
89 | (let [creds {:username "jartsa" :password "joo"}
90 | login-ret (test-config/call-api :post "login" nil creds)
91 | _ (log/debug (str "Got login-ret: " login-ret))
92 | token (get-in login-ret [:body :token])
93 | params {:x-token token}
94 | get-ret (test-config/call-api :get "/product-groups" params nil)
95 | status (:status get-ret)
96 | body (:body get-ret)
97 | right-body {:ret "ok", :product-groups [{:pgId 1, :name "Books"} {:pgId 2, :name "Movies"}]}]
98 | (is (= true (not (nil? token))))
99 | (is (= 200 status))
100 | (is (= right-body body)))))
101 |
102 |
103 | (deftest products-test
104 | (log/debug "ENTER products-test")
105 | (testing "GET: /api/products"
106 | (let [creds {:username "jartsa" :password "joo"}
107 | login-ret (test-config/call-api :post "login" nil creds)
108 | _ (log/debug (str "Got login-ret: " login-ret))
109 | token (get-in login-ret [:body :token])
110 | params {:x-token token}
111 | get-ret (test-config/call-api :get "/products/1" params nil)
112 | status (:status get-ret)
113 | body (:body get-ret)]
114 | (is (= (not (nil? token)) true))
115 | (is (= 200 status))
116 | (is (= "ok" (:ret body)))
117 | (is (= 1 (:pgId body)))
118 | (is (= 35 (count (:products body)))))))
119 |
120 |
121 | (deftest product-test
122 | (log/debug "ENTER product-test")
123 | (testing "GET: /api/product"
124 | (let [creds {:username "jartsa" :password "joo"}
125 | login-ret (test-config/call-api :post "login" nil creds)
126 | _ (log/debug (str "Got login-ret: " login-ret))
127 | token (get-in login-ret [:body :token])
128 | params {:x-token token}
129 | get-ret (test-config/call-api :get "/product/2/49" params nil)
130 | status (:status get-ret)
131 | body (:body get-ret)]
132 | (is (= (not (nil? token)) true))
133 | (is (= 200 status))
134 | (is (= "ok" (:ret body)))
135 | (is (= 2 (:pgId body)))
136 | (is (= 49 (:pId body)))
137 | (is (= {:pId 49,
138 | :pgId 2,
139 | :title "Once Upon a Time in the West",
140 | :price 14.4,
141 | :director "Leone, Sergio",
142 | :year 1968,
143 | :country "Italy-USA",
144 | :genre "Western"}
145 | (:product body))))))
146 |
147 |
148 |
--------------------------------------------------------------------------------