Add clojure demos site to repository.

This commit is contained in:
Oliver 2026-07-24 12:25:30 +01:00
parent 0232946798
commit 10041e2365
30 changed files with 3046 additions and 0 deletions

1
.gitignore vendored
View File

@ -38,3 +38,4 @@ projects/**/pom.xml
/.lsp/
/node_modules/
/.clj-kondo/*
*\\.shadow-cljs

View File

@ -0,0 +1,10 @@
((clojurescript-mode .
((cider-clojure-cli-aliases . ":dev")
(cider-preferred-build-tool . clojure-cli)
(cider-default-cljs-repl . custom)
(cider-custom-cljs-repl-init-form . "(do (user/cljs-repl))")
;(cider-shadow-default-options . "<your-build-name-here>")
(cider-shadow-watched-builds . (":app"))
(eval . (progn (make-variable-buffer-local cider-jack-in-nrepl-middlewares)
(add-to-list cider-jack-in-nrepl-middlewares "shadow.cljs.devtools.server.nrepl/middleware")))
)))

View File

@ -0,0 +1,33 @@
kind: pipeline
name: default
steps:
- name: npm-deps
image: node
commands:
- npm install
#- npx shadow-cljs build app
- name: Build
image: cimg/clojure:1.11.1-node #clojure:tools-deps
user: root
commands:
#- cp resources/public/rename-me-index.html resources/public/index.html
- clojure -Mbuild release app
#- clj -A:prod
- name: deploy-site
pull: True
image: appleboy/drone-scp
settings:
host: digitaloctave.com
username:
from_secret: ssh_user
key:
from_secret: ssh_key
port: 22
duration: 4m
## strip components removes parts of path at destination
##strip_components: 1
target: /var/www/clojure-demos/
source:
- resources/public/*

View File

@ -0,0 +1,31 @@
image: clojure:tools-deps-alpine
# unit-test:
# stage: test
# when: always
# image: clojure:tools-deps-alpine
# script:
# - cd ./reagent-reitit-demo/
# - clj -m figwheel.main --build dev --repl
# - export GITLIBS=".gitlibs/"
# - clojure -Sdeps '{:mvn/local-repo "./.m2/repository"}' -A:test
pages:
stage: deploy
when: always
image: clojure:tools-deps-alpine
script:
- cd ./reagent-reitit-demo/
- clj -M -m figwheel.main --build dev --repl
# script:
# - python setup.py develop
# - elcato build --path=public/
# - gzip --keep --recursive public
artifacts:
paths:
- resouorces/public
only:
- master
- develop

View File

@ -0,0 +1,23 @@
#+TITLE: Getting started
* Install dependencies and start app
#+BEGIN_SRC sh
npm install
#+END_SRC
#+BEGIN_SRC sh
clojure -Mbuild watch app
#+END_SRC
Future Content
- Dynamic html with macro for frontend generation on build.
- Complete frontend backend example wth ring retit reagent
https://stacksorted.com
https://www.clojuriststogether.org/news/call-for-new-proposals.-june-survey-results./

View File

@ -0,0 +1,11 @@
#+TITLE: About
#+DESCRIPTION: Examples mostly oriented around web development
* Introduction
This site has various example's guides and snippets showing how to use Clojure and ClojureScript, they are mainly based on my journey learning coming from JavaScript and python having no previous experience with java or the JVM.
Some of the struggles have been around Terminology & inter-op with the host languages along side re orientating my brain to think more functionally.
https://www.youtube.com/embed/LKtk3HCgTa8

View File

@ -0,0 +1,22 @@
#+TITLE: Example chat app
#+BEGIN_SRC clojure
(def user-list-item [user]
[:article.dt.w-100.bb.b--black-05.pb2.mt2 {:href "#0"}
[:div.dtc.w2.w3-ns.v-mid
[:img.ba.b--black-10.db.br-100.w2.w3-ns.h2.h3-ns {:src "http://mrmrs.github.io/photos/p/2.jpg"}]]
[:div.dtc.v-mid.pl3
[:h1.f6.f5-ns.fw6.lh-title.black.mv0 "Young Gatchell "]
[:h2.f6.fw4.mt0.mb0.black-60 "@yg"]]
[:div.dtc.v-mid
[:form.w-100.tr
[:button.f6.button-reset.bg-white.ba.b--black-10.dim.pointer.pv1.black-60 {:type "submit"} "+ Follow"]]]])
(def user-list [users]
(into
[:main.mw6.center]
(mapv user-item-list user)))
#+END_SRC

View File

@ -0,0 +1,112 @@
#+TITLE: Clojure(script) CI
* Ci integration
Building a polylith based clojure project, these steps are similar to most clojure projects with a few additions for the poly tool.
* Github
** Deploy rules
Name our action and run it only when code is pushed into master
#+BEGIN_SRC yaml :tangle .github/workflows/example.yaml
name: Staging Deploy
on:
push:
branches: [master]
#+END_SRC
** Checkout
Checkout repository, fetch depth is required if you needs tags available
#+BEGIN_SRC yaml :tangle .github/workflows/example.yaml
# Checkout the code
- uses: actions/checkout@v2
with:
fetch-depth: 0
#+END_SRC
** Cache
Cache the downloaded libraries so we don't do each each time.
#+BEGIN_SRC yaml :tangle .github/workflows/example.yaml
# Cache project dependencies
- name: Cache deps
uses: actions/cache@v2
with:
path: |
~/.polylith
~/.m2
~/.gitlibs
~/.clojure
key: ${{ runner.os }}-maven-${{ hashFiles('deps.edn') }}
restore-keys: |
${{ runner.os }}-maven-
${{ runner.os }}-
#+END_SRC
** Set env / project checks
Create a var called CHANGED_PROJECTS we can use in conditionals in later build steps, we also check our project comply's to the polylith rules before moving onto linting, testing, building and deploying.
#+BEGIN_SRC yaml :tangle .github/workflows/example.yaml
# Check the project and set enc for later steps
- name: Poly Check / Set Env
run: |-
cd workspace
echo "CHANGED_PROJECTS=$(clojure -M:poly ws get:changes:changed-or-affected-projects since:previous-release skip:dev)" >> $GITHUB_ENV
clojure -M:poly info since:release
clojure -M:poly check
#+END_SRC
** Setup Java
Install a version on java using the java action.
#+BEGIN_SRC yaml :tangle .github/workflows/example.yaml
# Install java
- name: Set up JDK & publish to maven
uses: actions/setup-java@v1
with:
java-version: 13
#+END_SRC
** Setup clojure
Install clojure from the clojure action.
#+BEGIN_SRC yaml :tangle .github/workflows/example.yaml
# Install clojure
- name: Install clojure tools
uses: DeLaGuardo/setup-clojure@3.5
with:
cli: 1.10.3.933
#+END_SRC
** Linting
Lint code with clj-kondo this example lints src and workspace folders.
#+BEGIN_SRC yaml :tangle .github/workflows/example.yaml
# Lint the code with kondo
- name: Code Linting
uses: DeLaGuardo/clojure-lint-action@master
with:
clj-kondo-args: --lint src workspace
check-name: Linting
github_token: ${{ secrets.GITHUB_TOKEN }}
#+END_SRC
** Testing changes since last release
Run test but only test since the last tagged release
#+BEGIN_SRC yaml :tangle .github/workflows/example.yaml
# Run the tests
- name: Poly Test
run: |-
cd workspace
clojure -M:poly test since:previous-release
#+END_SRC
** Building
Finally create your uberjar, this example uses a condition so it only builds if the projects or one of its components has changed.
We need to set the env in a previous step to make it available to future steps.
#+BEGIN_SRC yaml :tangle .github/workflows/example.yaml
# Build the uberjar
- name: Build my example api
if: contains('${{ env.CHANGED_PROJECTS }}', 'my-example-api')
run: |-
cd workspace/projects/my-example-api
clojure -X:uberjar
#+END_SRC
** Deploying

View File

@ -0,0 +1,323 @@
#+TITLE: Getting started with Clojure some of the basics.
* Clojure basics
Clojure is a very dynamic and interactive language that can give you very in-depth feedback on your running programs. For example, you can use Clojure to:
- Debug your code more easily by seeing the values of variables, the call stack, and the execution time of each function.
- Experiment with different ideas more quickly by evaluating code live inside an IDE.
- Use third party graphical tools to aid debugging and testing your code like portal, portfolio & clerk
The code below can be evaluated live inside the page you can select parts of the code to evaluate partially you can also modify the code the new version will be evaluated live.
#+BEGIN_SRC edn :export none :results silent :tangle deps.edn
{:paths ["src" "resources"]
:deps
{org.clojure/clojure {:mvn/version "1.10.0"}
org.clojure/clojurescript {:mvn/version "1.11.60"}}}
#+END_SRC
#+BEGIN_SRC text :export none :results silent :tangle readme.org
#+TITLE: Getting started
#+END_SRC
** Clojure comments
There a few ways to comment code in this language.
- Semi colon's can be used to comment out a whole line handy for adding in detailed comments, for commenting out code you can also use semi colons but more useful is the #_ syntax.
- #_ prefixed before a bracket will comments out everything between the opening and closing bracket, this is most hopefull to comment out entire blocks in one go.
- Another method is to use rich comments which is a function which stops the evaluation of everything inside the block, this is extremely useful as you can run the code from your IDE but it will not be executed in production.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
; comments are semi colon, linters will treat alignment different bassed on the number of semi colons
; comment out the block of code from starting bracket to matching closing bracket
; in this example the whole if condition is commented out which spans 3 lines
#_(if true
(prn "true")
(prn "false"))
; Ignore this code, but we can still eval it inside an IDE
(comment
(+ 1 2 3))
#+END_SRC
** Basic datatype's
Clojure has a rich set of data types, including numbers, strings, characters, booleans, and nil.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
;; comments are semi colon, linters will treat alignment different bassed on the number of semi colons
;;Integers
1234
;;Doubles
1.234
;;BigDecimal
2.33M
;;Ratios are allowed
34/3
;;Strings are double quoted
"Hello World"
;;characters are backslash escaped
\a \b \c \e \t \c
;;Symbols (named things like function & variable) are just text
;;these are quoted with a ' to stop evaluation, we have not actually defined them yet
;;this stops evaluation
'my-var-name 'my-fn-name 'my-second-var-etc 'my-second-fn-etc
;;hash map or dictonary keys are prefixed with a colon, no need to quote
:key1 :key2 :key3
;;booleans
true false
;; Null or None is just nil
nil
;; Regex patterns are strings prefixed with a hash symbol
(clojure.string/replace "find & replace in a string" #"&" "and" )
#+END_SRC
** Collections of data
Clojure also has a number of compound data types, such as lists, vectors, maps, and sets.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
;; List are wrapped in brackets, space or comma act as seerators
;; Lists are special in that they need to be escaped to be treated as data
;; using the list function or the quote symbol, which is short hand and explained later.
(list 1 2 3 4 5 6 7 8 9)
'(1 2 3 4 5 6 7 8 9)
;;Vectors or indexed lists are created with square brackets
;;or using the long form vector function
[1 2 3 4 5 6 7 8 9]
(vector 1 2 3 4 5 6 7 8 9)
;; hash-maps are denoted with curly braces or calling hash-map function
;; they must always contain key value pairs.
{:key-one 1 :key-two 2}
(hash-map :key-one 1 :key-two 2)
;; sets also use curly braces but start with a # or you can call hash-set function
;; sets have to be unique so duplicates will throw an error
#{1 2 3}
(hash-set 1 2 3)
;;#{1 2 1 3} how ever would through an error, using hash-set would remove the duplicate with out error.
#+END_SRC
* Clojure syntax
You have basically just learnt the syntax, clojure's syntax is made from the same data structures described above this is meant literally and is called EDN.
The clojure language is super simple it is always a function call followed by arguments, this applies for things like standard conditionals =and=, =or=, =not= etc
All functions return a value, this is always the last statement called inside the function.
When ever you see an opening bracket the value after is always a function unless the bracket is preceded by a character in which case this is a macro a common macro is ='(1 2)= which is the same as typing (list 1 2) so a function is still the first parameter.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
;; this code add's two number together, its starts with a bracket and call a function called + with 2 parameters
(+ 1 1)
;; this code now creates a list and does not execute the + function
;; it now returns 3 items in the list the function and the two numbers
(list + 1 1)
#+END_SRC
** Maths
All maths operators are also functions, meaning you start with the operation then the numbers to apply the operator against, opposed to many other languages where you would separate the numbers by operators.
This goes back to function as the first item in a list then the arguments.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
;; This is equivalend to 1 + 2 + 3 in other languages
(+ 1 2 3)
;; When using multiple operators just nest them.
(* 2 (+ 1 2 3))
#+END_SRC
** Variables
Variables in clojure are defined with =def= function, how ever unlike other languages you can not change these unless using some construct which allows the value to be changed like an atom.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
;; These define variable Which can not change
(def my-string "A string once set does not change")
(def my-number 3.14)
;; If you want to change your variables you need to define your variable as an atom
;; Where possible you want to avoid this, the language is immutable by design to help avoid bugs
;; any variable that can be arbitarily changed like atom's means any piece of code can change the value
;; and potentially cause issues when used else where in functions that did not expect the new value
(def my-atom-string (atom "A string once set does not change"))
(def my-atom-number (atom 3.14))
(def my-atom-hashmap (atom {:one 1 :two 2}))
;;One important thig to remember is that to access the values of an atom you need to prepend an @
;;when using functions that change an atom you do not need the @
;;@ is shorthand for the deref function
@my-atom-string
@my-atom-number
@my-atom-hashmap
;; Atoms are changed by calling functions which change the value in a thread safe fashion
;; reset! is to replace the value, swap! is used to update a value and takes a function to apply the update
(reset! my-atom-string "My new string")
;; Append to the existing string
(swap! my-atom-string str " appended text")
;; replace with empty hash map
(reset! my-atom-hashmap {})
;; add new key and value using assoc function
(swap! my-atom-hashmap assoc :key-one "value")
;; There is also defonce which is handy for hot reloading
;; when your code is reloaded all variables will be reset to the inital states
;; with defonce the values are maintained over reloads which helps when testing user flows
;; by maintining the state the user does not need to start again
(defonce my-atom-hashmap (atom {:ex-one "hi"}))
(swap! my-atom-hashmap assoc :key-one "value")
#+END_SRC
** Conditionals
Similar rules apply to if conditions =if=, =and=, =or= and =not= are also functions, it's also worth noting that all these function return values in other languages like python you would update a variable and the if would not directly return a value, try evaluating these to get a feel for this.
When needing multiple conditions look into, =cond= =condp= or =case=
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
;; OR AND NOT can be used by there own as function calls
(or nil 1)
(and nil 1)
(not true)
;; These can then be mixed with the if condition function like so
;; its important to note that the first parameter is the condition to check
;; then the code to call followed by the else function, you can only call a single function
(if (true? (not true))
1
2)
;; If you don't need the else your better of using when
(when (= 1 1)
(prn "True so print me!")
(prn "multiple statements aloowed in a when, only last returns a value"))
;; If you need to call multiple function when only one is allowed use the do function.
(if (true? (not true))
(do
(prn "first thing todo")
(prn "second thing todo, last statement will be the return")
1)
2)
#+END_SRC
** Looping
For the most part you will use =map= =filter= and =reduce= which apply a function for each item in a sequence of values, for the most part this is enough.
You can also use loop which takes your initial values as params inside the [] block and you call recur supplying the updated values in each iteration, not calling recur will end the loop.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
;; set n to 0 initially, increment in each loop while n is less than 5 return n value when this is no longer the case
(loop [n 0]
(if (< n 5)
(recur (inc n)) n))
#+END_SRC
For loops are also available in a similar fashion to =loop=
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
;; loop over the sequence of numbers, assigning each to X until the end
;; run any functions on the value in the rest of the body
(for [x [1 2 3 4 5 6]]
(* x x))
#+END_SRC
** Hashmap's keyword's & De-structuring
One of the fundamentals to working with hashmap's in clojure is that keywords are function when ever you see =:my-key= you are actually calling a function this is very different to most other languages.
This has some really nice side effects, one being it is very easy to navigate your hasahmap's the example below shows how to pull out the value 2.
*** Basic value fetching
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
(def my-hashmap {:top-lvl-key {:first-key 1 :second-key 2}})
;; you can get the value in a number of ways.
;; using neted get return the result of one get to the next
(get (get my-hashmap :top-lvl-key) :second-key)
;; much nicer is to use get-in and specify the path
(get-in my-hashmap [:top-lvl-key :second-key])
;; You can also call the keywords as a function
(:second-key (:top-lvl-key my-hashmap))
;; or using something called a threading macro
;; push the map through the :top-lvl-key function then the result
;; into the :second-key function
(-> my-hashmap :top-lvl-key :second-key)
#+END_SRC
You can use de-structuring in clojure to explode out keys when passed to a function.
Using =:or= we can set default values if the key is missing, the :as keyword can be used to access the unstructured map.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
(defn my-fn [{:keys [id name value missing] :or {missing "not set"} :as my-hashmap}]
(clojure.string/join " - " [id name value missing my-hashmap]))
(my-fn {:id 1 :name "bob" :value 456})
#+END_SRC
* Wierd symbol's
There is a good reference on the symbols in clojure in the link below when you encounter one your not sure about.
https://clojure.org/guides/weird_characters
Below you will find some simple examples, it is worth noting that some of the wierd symbols are just shorthand for a longer function name calls.
=:= Colon indicates a keyword's
;; :: Double colon makes a namespaced keyword, what ever is defined at the top of your file under (ns)
;; will be pre pended to the keyword
;; , comma is just white space and is used only for readability to the user
;; #( is an annoymous function (fn [param1] (prn param1)) is equivalent to #(prn %) % being param1
;; you can also use %1 %2 %3 etc to refernce other params, longer form is prefered because the params are named
;; but for very short small functions this variant can be handy
;; -> is the threading macro, basically the result of each statement is passed to the next
;; as the first parameter, this works nicely with hashmaps because keywords are functions
;; ->> same as above but the result is the last parameter
;; when working with sequences you tend to use this one most sequence functions Take
;; a sequence as the last parameter
;; '( this is the same as writting (list 1 2 3) the ' denotes we are using the list function
;; #_ this is the comment block it
#+BEGIN_SRC clojure :results verbatim :tangle src/core.cljc
;; : Colon indicates a keyword's
;; :: Double colon makes a namespaced keyword, what ever is defined at the top of your file under (ns)
;; will be pre pended to the keyword
;; , comma is just white space and is used only for readability to the user
;; #( is an annoymous function (fn [param1] (prn param1)) is equivalent to #(prn %) % being param1
;; you can also use %1 %2 %3 etc to refernce other params, longer form is prefered because the params are named
;; but for very short small functions this variant can be handy
;; -> is the threading macro, basically the result of each statement is passed to the next
;; as the first parameter, this works nicely with hashmaps because keywords are functions
;; ->> same as above but the result is the last parameter
;; when working with sequences you tend to use this one most functions Take
;; a sequence as the last parameter
;; '( this is the same as writting (list 1 2 3) the ' denotes we are using the list function
;; #_ this is the comment block it
#+END_SRC

View File

@ -0,0 +1,48 @@
#+TITLE: Containerizing your application
This is a multistage Dockerfile it has a build and run stage to help reduce the final image size.
This example is built for use with polylith it allows you to build multiple containers from a single docker file.
#+BEGIN_SRC yaml :tangle src/core.cljc
# Beware the alpine images, seems the googleads library does not like the alphine glib alternative ie muscl
FROM clojure:temurin-17-tools-deps AS builder
ENV CLOJURE_VERSION=1.11.1.1182
ARG PROJECT
RUN mkdir -p /build
WORKDIR /build
# Fetch the deps first, by copying just the deps we can cache the download
# except when the deps file has changed.
COPY ./deps.edn /build
RUN clojure -P -X:dev
COPY ./ /build
RUN clojure -T:build uberjar :project $PROJECT
FROM eclipse-temurin:17
ARG PROJECT
COPY --from=builder /build/target/ /app
WORKDIR /app
ADD ./projects/car-cli/resources/google/ /app/
ADD ./projects/car-cli/resources/google/ /root/
RUN mkdir -p /app/environment/
# Build ARG are not available at runtime but we can insert them into env as part of the build
ENV PROJECT_RUN=/app/${PROJECT}.jar
ENTRYPOINT ["java", "-jar"]
# this is expanded by a shell so can't use in a params list, can't find another way
CMD java -jar ${PROJECT_RUN}.jar
#+END_SRC
#+BEGIN_SRC sh :tangle src/core.cljc
docker build -f Dockerfile-Multistage . --build-arg PROJECT=example-api
#+END_SRC

View File

@ -0,0 +1,136 @@
#+TITLE: Datalog DSL Guide
* Intro to datalog
This is a short guide on writing common datalog queries to allow you to query databases like datascript, datalevin, datahike datomic & xtdb.
These examples are all aimed at datascript which is a browser client side database allowing interactivity int the examples.
You can find some further guides and information on the subject at these locations.
https://max-datom.com/
http://www.learndatalogtoday.org/
https://www.youtube.com/watch?v=oo-7mN9WXTw
Blog of the dev who makes datascript
https://tonsky.me/blog/the-web-after-tomorrow/
** Entities Attributes & values
Before getting started with data log it vital to understand data is stored as a list of values, the entity id which is a way to look up and group related attributes, attributes are the name of the thing you want to store so =:user/name= for example and the value to store under this name, the entity id will be generated when inserting data.
Sometimes the database may store other details like transaction time and revoked data dependant on the characteristics of the underlying database.
This is an example of how a EAV triplet would be represented.
#+BEGIN EXAMPLE
[1 | :user/name | "Daisy"]
#+END_EXAMPLE
This is an example of how a EAVT quadruplet would be represented, T in this instance is the Transaction ID if multiple values are inserted at once they would have the same transaction ID.
#+BEGIN EXAMPLE
[1 | :user/name | "Daisy" | 100]
#+END_EXAMPLE
** Creating a DATABASE
Datalog databases can be schema less but a lot of the power comes from creating a schema specifying uniqueness and relations on the stored fields.
You can create a new database using create-conn as below then empty hash map is simply a blank schema,
#+BEGIN_SRC clojurescript :results verbatim :tangle ./src/test.cljs
(def demo-conn (d/create-conn {}))
@demo-conn
#+END_SRC
Using the connection we can just start inserting data, using standard hash maps and lists structures, we always specify the attribute and the value when transacting.
#+BEGIN_SRC clojurescript :results verbatim :tangle ./src/test.cljs
(def demo-conn (d/create-conn {}))
(d/transact!
demo-conn
[{:user/name "Brooke" :user/img "me.jpg"}
{:user/name "Kalvin" :user/img "you.jpg"}])
@demo-conn
#+END_SRC
*** Creating a Schema
Not all datalog databases let you create a schema, but datascript does this allows us to add constraints and relations between the stored data.
In this example we are saying =:user/name= is unique and =:user/rooms= has a many to one relationship, when we later transact data it will use the constraints to stop things like duplicates from being created.
#+BEGIN_SRC clojurescript :results verbatim :tangle ./src/test.cljs
(def schema {:user/name {:db/unique :db.unique/identity}
:user/rooms {:db/cardinality :db.cardinality/many
:db/valueType :db.type/ref}
;; needs to be set so we can aggregate into the find query
:diagram/objects {:db/cardinality :db.cardinality/many
:db/valueType :db.type/ref}
})
(def demo-conn (d/create-conn schema))
#+END_SRC
** Transacting data
Transacting this data would mean Brooke would be inserted once but the image will be updated to =you.jpg=
#+BEGIN_SRC clojurescript :results verbatim :tangle ./src/test.cljc
(str (d/transact! demo-conn [{:user/name "Brooke" :user/img "you.jpg"}]))
#+END_SRC
*** Transacting related data
We can insert bulk data and include relationship information by specifying negative id's in the transaction maps.
The example below adds =circle= and =square= to =:diagram/objects= the negatives being replaced by the real entity ids.
#+BEGIN_SRC clojurescript :results verbatim :tangle ./src/test.cljc
(d/transact! demo-conn [{:db/id -1 :object/name "circle"}
{:db/id -2 :object/name "square"}
{:db/id -3 :object/name "rectangle"}
{:diagram/objects [-1 -2]}])
(str @demo-conn)
#+END_SRC
** Querying the databases
There are three types of queries in datalog entity lookup's pulling a tree of data or querying with =d/q=.
*** Looking up an entity
=d/entity= is used to find the entity id, using any unique piece of data for example the user =Brooke= exists once so the entity db/id will be returned which can be used for further queries.
#+BEGIN_SRC clojurescript :results verbatim :tangle ./src/test.cljc
(d/entity @demo-conn [:user/name "Brooke"])
#+END_SRC
*** Pull a tree of data
Pull is used with entity id's once you know the entity you can specify what data you want to view ='[*]= being the most common looking up all keys, you can also specify the attributes your interested in looking up including there relations to make a more specific view.
#+BEGIN_SRC clojurescript :results verbatim :tangle ./src/test.cljc
(d/pull @demo-conn '[*] 1)
(d/pull @demo-conn '[:user/name :user/rooms] 1)
#+END_SRC
*** Querying your dataset
Querying in datalog is all about binding variables to your entities attributes and values which you can use in you conditions or to return in the result set.
In this example we return the user-id and user/name in the find clause which we looked up in the where clause by finding all attributes =:user/name= the binding the entity id and username to variables on each match to display in the find clause.
#+BEGIN_SRC clojurescript :results verbatim :tangle ./src/test.cljc
(d/q '[:find ?user-entity ?user-name :where
[?user-entity :user/name ?user-name]] @demo-conn)
#+END_SRC
#+BEGIN_SRC clojurescript :results verbatim :tangle ./src/test.cljc
(d/q '[:find [(pull ?e [*]) ]
:where
[?e :object/name ?objects]]
@demo-conn)
#+END_SRC
Fetch a diagram and look up the associated objects
#+BEGIN_SRC clojurescript :results verbatim :tangle ./src/test.cljc
(d/q '[:find [(pull ?diagram-entity [* {:diagram/objects [:object/name]}])]
:where
[?diagram-entity :diagram/objects ?objects]]
@demo-conn)
#+END_SRC
Write some more as needed better examples in the src code.

View File

@ -0,0 +1,260 @@
#+TITLE: Embedding maps in your apps
#+DESCRIPTION: Example's of embeding maps into your frontend application.
#+FILETAGS: clojurescript:frontend:interop
* Introduction
Below you will find some simple examples of using map api's inside clojure, you can use the inline eval for some of the example where sci supports a specific library.
Install npm dependencies
** Setup for downloaded version
Install the npm dependencies for react then launch shadow via the terminal or jack in via your IDE.
#+BEGIN_SRC html :results silent :exports none :tangle readme.org
Install dependencies with.
npm install
Run the project with the below command.
npx shadow-cljs watch app
Alternatively jack into the project from your ide.
#+END_SRC
* Google Maps
Below is an example of using google maps, it pulls in the library directly you could alternatively use an npm dependency.
You will need to provide your own api key, the examples use no api key and render a warning so paste in your google api key to make this work.
#+BEGIN_SRC html :results silent :exports none :tangle resources/public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Clojure demos</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="app">
App loading here
</div>
<script src="/cljs-out/main_bundle.js" type="application/javascript"></script>
</body>
</html>
#+END_SRC
#+BEGIN_SRC edn :results silent :exports none :tangle deps.edn
{:paths ["src" "resources"]
:deps
{org.clojure/clojure {:mvn/version "1.10.0"}
org.clojure/clojurescript {:mvn/version "1.11.60"}
funcool/promesa {:mvn/version "11.0.674"}
reagent/reagent {:mvn/version "1.2.0"}
thheller/shadow-cljs {:mvn/version "2.24.0"}}}
#+END_SRC
#+BEGIN_SRC edn :results silent :exports none :tangle shadow-cljs.edn
{:deps {:aliases [:dev]}
:dev-http {8080 ["resources/public/" "classpath:public"]}
:source ["src" "../../components"]
:builds {:app {:output-dir "resources/public/cljs-out/"
:asset-path "/cljs-out"
:target :browser
:compiler-options {:infer-externs :auto
:externs ["datascript/externs.js"]
:output-feature-set :es6}
:modules {:main_bundle {:init-fn clojure-demo.core/startup!}}
:devtools {:after-load app.main/reload!}}}}
#+END_SRC
#+BEGIN_SRC json :results silent :exports none :tangle package.json
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"shadow-cljs": "^2.23.3",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
}
}
#+END_SRC
#+BEGIN_SRC json :exports none :results output :tangle src/clojure_demo/core.cljs
(ns clojure-demo.core
(:require
["react-dom/client" :refer [createRoot]]
[cljs.core.async :as async]
[cljs.core.async.interop :as async-in]
[promesa.core :as promesa]
[shadow.cljs.modern :refer [js-await] :as shadow]
[reagent.core :as reagent]))
#+END_SRC
** Load in the google maps script by adding it to the dom
Code pulled from google
#+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs
(defn load-google-maps-script [api-key]
(let [script (.createElement js/document "script")]
;; copied from googles recommended way of loading google maps
(set! (.-innerHTML script) (str "(g=>{var h,a,k,p=\"The Google Maps JavaScript API\",c=\"google\",l=\"importLibrary\",q=\"__ib__\",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement(\"script\"));e.set(\"libraries\",[...r]+\"\");for(k in g)e.set(k.replace(/[A-Z]/g,t=>\"_\"+t[0].toLowerCase()),g[k]);e.set(\"callback\",c+\".maps.\"+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+\" could not load.\"));a.nonce=m.querySelector(\"script[nonce]\")?.nonce||\"\";m.head.append(a)}));d[l]?console.warn(p+\" only loads once. Ignoring:\",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
key: \"" api-key "\", v: \"weekly\"});"))
(.appendChild (.-head js/document) script)))
#+END_SRC
** Create a map using google and core async
In this example we render google maps using core async, async go blocks drop type hints so we need to pull out any hinting to functions, this is a shame as this version reduces the amount of nesting.
#+BEGIN_SRC clojurescript :results none :tangle src/clojure_demo/core.cljs
(defn google-map-core-async []
(let [map-element (reagent/atom nil)
;; pulled out of the go block to satisfy the infer warnings, the go block removes hints
get-map-obj (fn [obj] (.-Map ^js/google.maps.Map obj))
get-marker-obj (fn [obj] (.-Marker ^js/google.maps.Map obj))]
(load-google-maps-script "")
;; go blocks loose hints so may cause infer warnings
(async/go (let [gMap (get-map-obj (async-in/<p! (js/google.maps.importLibrary "maps")))
gMarker (get-marker-obj (async-in/<p! (js/google.maps.importLibrary "marker")))
map (gMap. @map-element
(clj->js {:center {:lng 131.031 :lat -25.344} :zoom 4}))]
(gMarker. (clj->js {:map map
:title "test marker 1"
:position {:lng 131.031 :lat -24.344}}) "marker 1")
(gMarker. (clj->js {:map map
:title "test marker 2"
:position {:lng 131.031 :lat -25.344}}) "marker 2")))
(fn []
[:div#google-map-async.m-auto
{:style {:width "400px" :height "400px"} :ref #(reset! map-element %)}
"core async map here"])))
#+END_SRC
** Create a map using google and shadow js-await
Shadow CLJS has its one js-await macro we can use in the following fashion, here we just use it to wait for the maps library to have loaded the maps and marker libraries before executing the code.
#+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs
(defn google-map-js-await []
(let [map-element (reagent/atom nil)]
(load-google-maps-script "")
(shadow/js-await
[js-map (js/google.maps.importLibrary "maps")]
(shadow/js-await
[js-marker (js/google.maps.importLibrary "marker")]
(when @map-element
(let [gMap (.-Map ^js/google.maps.Map js-map)
gMarker (.-Marker ^js/google.maps.Marker js-marker)
map (gMap. @map-element
(clj->js {:center {:lng 131.031 :lat -25.344} :zoom 4}))]
(gMarker. (clj->js {:map map
:title "test marker 1"
:position {:lng 131.031 :lat -24.344}}) "marker 1")
(gMarker. (clj->js {:map map
:title "test marker 2"
:position {:lng 131.031 :lat -25.344}}) "marker 2")
nil))))
(fn []
[:div#google-map-js-await.m-auto
{:style {:width "400px" :height "400px"} :ref #(reset! map-element %)}
"shadow js-await map here"])))
[google-map-js-await]
#+END_SRC
** Create a map using google and promesa
An example using promesa to render a google map.
#+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs
(defn google-map-promesa []
(let [map-element (reagent/atom nil)]
(load-google-maps-script "")
(promesa/let
[js-map (js/google.maps.importLibrary "maps")
js-marker (js/google.maps.importLibrary "marker")]
(let
[gMap (.-Map ^js/google.maps.Map js-map)
gMarker (.-Marker ^js/google.maps.Marker js-marker)
map (gMap. @map-element
(clj->js {:center {:lng 131.031 :lat -25.344} :zoom 4}))]
(gMarker. (clj->js {:map map
:title "test"
:position {:lng 131.031 :lat -24.344}}) "marker 1")
(gMarker. (clj->js {:map map
:title "test"
:position {:lng 131.031 :lat -25.344}}) "marker 1")))
(fn []
[:div#google-map-promesa.m-auto
{:style {:width "400px" :height "400px"} :ref #(reset! map-element %)}
"promesa map here"])))
[google-map-promesa]
#+END_SRC
#+BEGIN_SRC clojure :exports none :tangle src/clojure_demo/core.cljs
(defn current-page []
[:div
[google-map-core-async]
[google-map-promesa]
[google-map-js-await]])
(defn mount-root-page []
;; this select the main node from the html file and injects your page content
(.render
(createRoot (.getElementById js/document "app"))
(reagent/as-element [current-page])))
(def startup! (mount-root-page))
#+END_SRC
* LibraMaps
Create a dummy function to load the js dynamically, this could be imported in other ways like npm.
#+BEGIN_SRC clojurescript :results value :tangle src/clojure_demo/core.cljs
(defn load-libra-maps []
(let [script (.createElement js/document "script")]
(.setAttribute script "src" "https://unpkg.com/maplibre-gl/dist/maplibre-gl.js")
(.appendChild (.-head js/document) script)))
(load-libra-maps)
#+END_SRC
We use =:ref= to grab a reference and call our function when it is created we can then get elements id and construct our map.
To customize see https://maplibre.org/maplibre-gl-js/docs/
#+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs
(defn libra-map-promesa []
(load-libra-maps)
(let [create-map (fn map-element [element]
(.-innerHTML element "")
(new js/maplibregl.Map
(clj->js {:container (.-id element)
:style "https://demotiles.maplibre.org/style.json"
:zoom 1})))]
(fn []
[:div#libra-map.m-auto
{:style {:width "400px" :height "400px"}
:ref create-map}
"libra map here"])))
[libra-map-promesa]
#+END_SRC

View File

@ -0,0 +1,182 @@
#+TITLE: Generating xml to produce a sitemap
* Introduction
In this example we generate a sitemap the example uses reader conditionals so the code works in clojure and clojurescript it also has a simple macro to output the file if used in the frontend.
#+BEGIN_SRC html :results silent :exports none :tangle readme.org
Install dependencies with.
npm install
Run the project with the below command
npx shadow-cljs watch app
Alternatively jack into the project from your ide.
Once launched you should see a sitemap.xml file generated inside this project.
#+END_SRC
#+BEGIN_SRC edn :results silent :exports none :tangle deps.edn
{:paths ["src" "resources"]
:deps
{org.clojure/clojure {:mvn/version "1.10.0"}
org.clojure/clojurescript {:mvn/version "1.11.60"}
org.clojure/data.xml {:mvn/version "0.2.0-alpha8"}
tick/tick {:mvn/version "0.6.2"}
reagent/reagent {:mvn/version "1.2.0"}
thheller/shadow-cljs {:mvn/version "2.24.0"}}}
#+END_SRC
#+BEGIN_SRC edn :results silent :exports none :tangle shadow-cljs.edn
{:deps {:aliases [:dev]}
:dev-http {8080 ["resources/public/" "classpath:public"]}
:source ["src" "../../components"]
:builds {:app {:output-dir "resources/public/cljs-out/"
:asset-path "/cljs-out"
:target :browser
:compiler-options {:infer-externs :auto
:externs ["datascript/externs.js"]
:output-feature-set :es6}
:modules {:main_bundle {:init-fn clojure-demo.core/startup!}}
:devtools {:after-load app.main/reload!}}}}
#+END_SRC
#+BEGIN_SRC json :results silent :exports none :tangle package.json
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"shadow-cljs": "^2.23.3",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
}
}
#+END_SRC
#+BEGIN_SRC html :results silent :exports none :tangle resources/public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Clojure demos</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="app">
App loading here
</div>
<script src="/cljs-out/main_bundle.js" type="application/javascript"></script>
</body>
</html>
#+END_SRC
** Clojure(script) sitemap generation function
This will work in clojure and clojurescript, the code is slightly different to account for missing feature in the clojurescript version.
We can use a macro to generate a sitemap from a client side app on startup, or just use it directly if generating on a server.
#+BEGIN_SRC clojure :exports none :tangle src/clojure_demo/sitemap.cljc
(ns clojure-demo.sitemap
(:require
[clojure.data.xml :as xml]
[tick.core :as tick]))
#+END_SRC
#+BEGIN_SRC clojure :results value :tangle src/clojure_demo/sitemap.cljc
;;#?(:clj (xml/alias-uri 'sitem "http://www.sitemaps.org/schemas/sitemap/0.9"))
(defn pwa-sitemap [domain routes]
#?(:cljs (xml/emit-str
{:tag :urlset
:attrs {#_#_:xmlns "http://www.sitemaps.org/schemas/sitemap/0.9"}
:content
(mapv (fn [r]
{:tag :url
:attrs {}
:content [{:tag :loc :attrs {} :content [(str domain r)]}
{:tag :lastmod :attrs {} :content [(tick/format :iso-zoned-date-time (tick/zoned-date-time))]}]}
) routes)})
:clj (xml/indent-str
{:tag :urlset
:attrs {:xmlns "http://www.sitemaps.org/schemas/sitemap/0.9"}
:content
(mapv (fn [r]
{:tag :url
:attrs {}
:content [{:tag :loc :attrs {} :content [(str domain r)]}
{:tag :lastmod :attrs {} :content [(tick/format :iso-zoned-date-time (tick/zoned-date-time))]}]}
) routes)})))
(defmacro spit-pwa-sitemap
"Generate the sitemap and outut to the provided path"
{:arglists '([body] [options & body]), :style/indent 0}
[path domain urls]
#?(:clj (spit path (pwa-sitemap domain urls))
:cljs nil))
(comment
(pwa-sitemap "https://example.com/" ["/one" "/two"])
(spit-pwa-sitemap "sitemap.xml" "https://example.com/" ["/one" "/two"]))
#+END_SRC
** Clojurescript usage
This is an example of creating the sitemap inside a frontend application, it call the macro which is evaluated at build and outputs the sitemap.xml
When working with data.xml it is very important to set the xml namespace to avoid an alias being appended to your xml, the namespace should be set in the root xml element and via =alias-uri= then make sure all element tag value are called via the namespace.
Also note namespacing is not supported in clojurescript so alias-uri is not available
Look into why :xmlns breaks sci rendering
Adding the k
#+BEGIN_SRC clojurescript :exports none :tangle src/clojure_demo/core.cljs
(ns clojure-demo.core
(:require
[clojure.data.xml :as xml]
["react-dom/client" :refer [createRoot]]
[reagent.core :as reagent]
[clojure-demo.sitemap :refer [pwa-sitemap]])
(:require-macros [clojure-demo.sitemap :refer [spit-pwa-sitemap]]))
#+END_SRC
Because our routes are just data we could add a =:sitemap= key for the handler maps and filter the routes based on the key or even run custom functions to build up the sitemap dynamically when the contest is generated from a database.
You can reuse the code in the cljc file the sitemap function is replicated here as sci does not work with the reader conditionals in the code.
#+BEGIN_SRC clojurescript :results value :tangle src/clojure_demo/core.cljs
;;Should be set in clojure but it is not supported in clojurescript currently
;;(xml/alias-uri 'sitemap "http://www.sitemaps.org/schemas/sitemap/0.9")
(def routes
[["/"
{:name ::frontpage
:view prn}]
["/about"
{:name ::about
:view prn}]
["/item/:id"
{:name ::item
:view prn
:parameters {:path {:id int?}}}]])
;; map first over the routes so we only get strings
(->> routes
(mapv first)
(pwa-sitemap "https://example.com" ))
#+END_SRC
#+BEGIN_SRC clojure :exports none :tangle src/clojure_demo/core.cljs
(defn current-page []
[:pre (str (pwa-sitemap "https://example.com" ["/one" "/two"]))])
(defn mount-root-page []
;; this select the main node from the html file and injects your page content
(.render
(createRoot (.getElementById js/document "app"))
(reagent/as-element [current-page])))
;; call our macro to generate the sitemap file
(spit-pwa-sitemap "sitemap.xml" "https://example.com" ["/one" "/two"])
(def startup! (mount-root-page))
#+END_SRC

View File

@ -0,0 +1,142 @@
#+TITLE: Intro to Hiccup
* Introduction
hiccup is a html DSL, used extensively in the clojure's eco-system, there are others as well but hiccup is the most widely used.
In hiccup everything is a list, this means you can easily compose html using standard language constructs.
To render the hiccup to html elements we call the (html) function from the hiccup library for server side with reagent or client side code this will likely be a different function like (render).
There are a few variant's the main ones being https://github.com/weavejester/hiccup and https://github.com/lambdaisland/hiccup , the lambdaisland one is particular helpful if you want to share html between the frontend and backend as its closer to reagents builtin hiccup.
#+BEGIN_SRC edn :results silent :export none :tangle deps.edn
{:paths ["src" "resources"]
:deps
{org.clojure/clojure {:mvn/version "1.10.0"}
org.clojure/clojurescript {:mvn/version "1.11.60"}}}
#+END_SRC
#+BEGIN_SRC text :results silent :export none :tangle readme.org
#+TITLE: Getting started
#+END_SRC
** Simple examples
If using reagent you don't need to pass into h/html but this is server side reagent also has some helpers to work with react nicer.
#+BEGIN_SRC clojure :tangle src/core.clj
[:span "bar"]
#+END_SRC
*** Styles classes and ID's
Attributes are added as a map of values styles are also a map
#+BEGIN_SRC clojure :tangle src/core.clj
[:span {:class "class1 class2" :title "my title" :style {:color "red"}} "bar"]
#+END_SRC
You can use shorthand to add id's and classes
#+BEGIN_SRC clojure :results output :tangle src/core.clj
[:span#id.class1.class2 "bar"]
#+END_SRC
** Example of manipulating hiccup with clojure functions
You can use clojure core language to manipulate these vectors.
place parts inside another containing element
#+BEGIN_SRC clojure :results output :tangle src/core.clj
(into [:div.container]
[[:span "span 1"]
[:span "span 2"]])
#+END_SRC
You could also use merge, in this example the spans are merged inside the div vector.
#+BEGIN_SRC clojure :results output :tangle src/core.clj
(merge [:div] [:span "span 1"] [:span "span 2"] [:span "span 3"])
#+END_SRC
We can take advantage of lazyness if we like
#+BEGIN_SRC clojure :results output :tangle src/core.clj
(defn navbar-link [{:keys [href title text] :or {text href title nil} :as link}]
[:a.link.dim.white.dib.mr3 {:key href :href href :title title} text])
[:div (into [:nav.f6.fw6.ttu.tracked] (vec (take 2 (mapv navbar-link
[{:key "link1" :href "link1" :title "title here"}
{:key "link2" :href "link2" :title nil}
{:key "link3" :href "link3" :text "link text"}
{:key "link4" :href "link4"}]))))]
#+END_SRC
** Compossible components
The main advantage comes from the ability to compose the parts together, so we can break our html apart and recombine using all the function at our disposal.
In this example our navigation is defined as a hash map, the data is separated out from the html, we can then pass the data to our component to render it.
In this example we have a link component and a nav component the link component take the values as key, value pairs and uses de structuring while also setting default if values are not set.
#+BEGIN_SRC clojure :results output :tangle src/core.clj
(defn navbar-link [{:keys [href title text] :or {text href title nil} :as link}]
[:a.link.dim.white.dib.mr3 {:key href :href href :title title} text])
(defn navbar [links]
[:header.bg-black-90.w-100.ph3.pv3.pv4-ns.ph4-m.ph5-l
(into [:nav.f6.fw6.ttu.tracked]
(mapv navbar-link links))])
[navbar [{:href "link1" :title "title here"}
{:href "link2" :title nil}
{:href "link3" :text "link text"}
{:href "link4"}]]
#+END_SRC
In this example we create some simple top trump style cards using a map with a vector of nested maps for the stats.
We use map and into to convert the stats value
#+BEGIN_SRC clojure :results output :tangle src/core.clj
(defn playing-card
[{:keys [title image stats]}]
[:article
[:div.border-solid.border-2.border-sky-500.m-4.w-64.rounded-lg
[:div.dt.w-full
[:div.m-4 [:h1.f5.f4-ns.mv0 title]]
[:img.db.w-full.h-100.br2.br--top {:src image}]]
(into [:ul.m-4]
(mapv (fn build-stats [[stat value]]
[:li [:div stat [:div.float-right (str value)]]])
stats))
[:p.f6.lh-copy.measure.mt-2.mid-gray ""]]])
[:div.flex.flex-wrap
[playing-card
{:title "Bee"
:image "https://loremflickr.com/300/300/bee"
:stats {:strength "6"
:lifespan "3" ;; 6 weeks
:danger "5"
:mobility "9"}}]
[playing-card
{:title "Spider"
:image "https://loremflickr.com/300/300/spider"
:stats {:strength "4"
:lifespan "5" ;; 1 year
:danger "8"
:mobility "5"}}]
[playing-card
{:title "Ant"
:image "https://loremflickr.com/300/300/ant"
:stats {:strength "8"
:lifespan "8" ;; 7 years
:danger "4"
:mobility "3"}}]
[playing-card
{:title "Dragonfly"
:image "https://loremflickr.com/300/300/dragonfly"
:stats {:strength "2"
:lifespan "1" ;;2 weeks
:danger "1"
:mobility "9"}}]]
#+END_SRC

View File

@ -0,0 +1,20 @@
#+TITLE: HoneySQL tips & tricks
* Basic statements
#+BEGIN_SRC clojurescript :tangle ./src/core.cljs
#_(ns honeysql.core
(:require
[honey.sql.core :as sql]
[honey.sql.helpers :refer :all :as helpers]))
#+END_SRC
** Simple table select, with filtering and sorting applied
#+BEGIN_SRC clojurescript :tangle ./src/core.cljs
(-> (sqlh/select :*)
(sqlh/from [:company])
(sqlh/where [:= :id 1])
(sqlh/format :pretty true))
#+END_SRC

View File

@ -0,0 +1,305 @@
#+TITLE: HoneySQL examples
docker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres
* Introduction
HoneySQL is a Domain Specific Language (DSL) for building SQL queries. HoneySQL provides a number of helper functions to make it easier to build queries, so you don't need to build the data structure manually.
HoneySQL does not care about connecting to your database. It only builds queries. All examples send the result to =format= and =first= so that the resulting query is displayed inside the browser.
#+BEGIN_SRC edn :results silent :exports none :tangle deps.edn
{:paths ["src" "resources"]
:deps
{org.clojure/clojure {:mvn/version "1.10.0"}
com.github.seancorfield/honeysql {:mvn/version "2.4.1045"}
}
:aliases {:run {:main-opts ["-m" "clojure-demo.core"]}}
}
#+END_SRC
#+BEGIN_SRC text :results silent :exports none :tangle readme.org
#+TITLE: Getting started
#+END_SRC
#+BEGIN_SRC clojure :tangle :exports none :tangle src/core.clj
(ns clojure-demo.core
(:require
["react-dom/client" :refer [createRoot]]
[reagent.core :as reagent]))
#+END_SRC
* Basic Query's
One nice way to write HoneySQL queries is to use the =->= threading macro. The =->= threading macro takes two arguments: the first argument is the result of the previous call, and the second argument is the function to call with that result. This allows you to chain together multiple calls to build complex queries.
Here is an example of how to use the =->= macro to build a simple SQL query:
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(-> (sqlh/select :first_name :last_name :email)
(sqlh/from :users)
(sql/format {:pretty true})
(first))
#+END_SRC
The equivalent code with out a threading macros looks like this.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(first
(sql/format
(sqlh/from
(sqlh/select :first_name :last_name :email)
:users)
{:pretty true}))
#+END_SRC
** Basic SQL select queries
Using HoneySQL you can incrementally build up our queries, they do not need to be complete to get an answer this allow us to define partial SQL and compose it together.
The example below will produce a select for a couple of fields passing the resulting data into format to create an SQL string we can send to the database, we call first because the result is a vector containing and SQL and any values need none in this instance.
#+BEGIN_SRC clojure :results verbatim
(-> (sqlh/select :first_name :last_name :email)
(sql/format {:pretty true})
(first))
#+END_SRC
We can extend the example above to include the =FROM= part of the SQL statement to give us something more complete.
How ever it's much nicer for readability to use the threading macro
#+BEGIN_SRC clojure :results verbatim
(-> (sqlh/select :first_name :last_name :email)
(sqlh/from :users)
(sql/format {:pretty true})
(first))
#+END_SRC
The functions understand SQL statement ordering so the order you call the functions does not matter.
#+BEGIN_SRC clojure :results verbatim
(-> (sqlh/from :users)
(sqlh/select :first_name :last_name :email)
(sql/format {:pretty true})
(first))
#+END_SRC
We can extend the query to add in limiting & ordering.
#+BEGIN_SRC clojure :results verbatim
(-> (sqlh/select :first_name :last_name :email)
(sqlh/from :users)
(sqlh/order-by :first_name)
(sqlh/limit 10)
(sql/format {:pretty true})
(first))
#+END_SRC
Now is a good time to explain aliasing, basically the values to select become wrapped in vectors with the second value being the alias so [:first_name :fn] to alias the column =first_name= to =fn= we can aliases columns tables sub select's same as standard SQL.
#+BEGIN_SRC clojure :results verbatim
(-> base-sql
(sqlh/select [:first_name :fn] [:last_name :ln] [:email :e])
(sql/format {:pretty true})
(first))
#+END_SRC
** Basic SQL filtering
Filtering is just as simple and support the usual operators like = < > we pass them in as keywords so =:== =:<>= =:<= =:.= would be the equivalents.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(-> (sqlh/select :first_name :last_name :email)
(sqlh/from :users)
(sqlh/where [:= :first_name "spot"]
[:= :last_name "dog"])
(sql/format {:pretty true})
(first))
#+END_SRC
Often we want to conditionally filter, this is nice and simple with the knowledge that the =where= function will short circuit given a nil value this means we can use =when= and =if= functions inside our sql generations.
So below the SQL where will not be appended because true is not false so the when returns nil removing the where in the generated query.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(-> (sqlh/select :first_name :last_name :email)
(sqlh/from :users)
(sqlh/where (when (true? false) [:= :first_name "spot"]))
(sql/format {:pretty true})
(first))
#+END_SRC
We can use similar technique to switch between matching a single and multiple values.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(defn conditional-where [id]
(-> (sqlh/select :*)
(sqlh/from [:company])
(sqlh/where (if (sequential? id) [:in :id id] [:= :id id]))))
(clojure.string/join
"\n"
[(-> (conditional-where [1 2 3])
(sql/format {:pretty true})
(first))
(-> (conditional-where 1)
(sql/format {:pretty true})
(first))])
#+END_SRC
** Composing SQL queries
For all the standard fn's like select and where there are equivalent merge fn's the merge versions append in place of replacing.
A good strategy is to build basic queries extending them when needed, so create a base select then create a function which build on the query adding in the conditions, this allow you to run the base queries in the REPL or the extended version making it easier to find query related issues by testing parts in isolation.
we can use =if= =when= =when-let= =cond->= among other functions to help build these, in the example below you can see the where part of the query is modified based on what values are provided in the map.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(def base-sql
(-> (sqlh/select :first_name :last_name :email)
(sqlh/from :users)))
(defn user-search [{:keys [first-name last-name] :or {first-name nil last-name nil}}]
(-> base-sql
(sqlh/select :first_name :last_name)
(sqlh/where (when first-name [:= :first_name first-name]))
(sqlh/where (when last-name [:= :last_name last-name]))))
(clojure.string/join
"\n"
[;; Search for furst name only
(-> {:first-name "spot" }
(user-search)
(sql/format {:pretty true})
(first))
;; Search for last name only
(-> {:last-name "dog"}
(user-search)
(sql/format {:pretty true})
(first))
;; Search for both first and last name
(-> {:first-name "spot" :last-name "dog"}
(user-search)
(sql/format {:pretty true})
(first))])
#+END_SRC
** Joining tables
We can also do joins to other table's
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(def base-sql
(-> (sqlh/select :first_name :last_name :email)
(sqlh/from :users)))
(def base-join-sql
(-> base-sql
(sqlh/join [:address] [:= :users.address_id :address.id])))
(first (sql/format base-join-sql))
#+END_SRC
or group by's and sql functions like =count= =max= =min= these can be used by appending :%name to the selected column.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(def base-group-sql
(-> base-sql
(sqlh/select :first_name [:%count.first_name :count_name])
(sqlh/group-by :first_name)))
(first (sql/format base-group-sql))
#+END_SRC
** Larger query
This is how I like to compose queries, and shows a larger query being generated.
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(def big-base-sql
(-> (sqlh/select :users.* :address.* :products.*)
(sqlh/from :users)
(sqlh/join :address [:= :users.address_id :address.id])
(sqlh/join :products [:= :users.address_id :address.id])
(sqlh/limit 100)))
(defn big-base-filters [filters]
(-> big-base-sql
(sqlh/where
(when (:first_name filters)
[:= :first_name (:first_name filters)]))
(sqlh/where
(when (:last_name filters)
[:= :last_name (:last_name filters)]))
(sqlh/where
(when (:product_name filters)
[:= :product.name (:product_name filters)]))
(sqlh/where
(when (:active filters)
[:= :active (:active filters)]))))
(first (sql/format
(big-base-filters
{:first_name "spot"
:last_name "dog"
:product_name "lead"
:active true})))
#+END_SRC
Don't forget its just data, if you don't use sql/format it just returns a data structure which you can build manually, or manipulate with the standard library.
#+BEGIN_EXAMPLE
; {:select (:first_name :last_name :email), :from (:users)}
#+END_EXAMPLE
** Extending / raw sql
When all else fails you have a few options, check to see if there is a honeysql db specific library or break out =sql/raw= or extending honey sql.
Say we want to get people added in the last 14 days this is a bit more tricky
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
[(def base-last-14-days-sql
(-> base-sql
(sqlh/where [:>
[:raw "created"]
[:raw "CURRENT_DATE - INTERVAL '14' DAY"]])))]
(first (sql/format base-last-14-days-sql))
#+END_SRC
* Basic statements
*** Switch between singular or multiple values in condition
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(defn conditional-where [id]
(-> (sqlh/select :*)
(sqlh/from [:company])
(sqlh/where (if (sequential? id) [:in :id id] [:= :id id]))))
(first (sql/format (conditional-where [1 2 3]) {:pretty true}))
;(clojure.string/join "" (sql/format (conditional-where [1 2 3]) {:pretty true}))
#+END_SRC
* Insert or update data on conflict
In this example we will insert some data but on a conflict we will update the row instead,
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(defn upsert-daily-sales-sql [values]
(-> (sqlh/insert-into :address)
(sqlh/values values)
(sqlh/on-conflict :first_name :last_name :email)
(sqlh/do-update-set :line1 :line2 :city :country :postcode)))
#+END_SRC
#+BEGIN_SRC clojure :results verbatim :tangle src/core.clj
(defn -main []
(first (sql/format (conditional-where [1 2 3]) {:pretty true}))
)
#+END_SRC
* Further reading
https://github.com/seancorfield/honeysql

View File

@ -0,0 +1,268 @@
#+TITLE: ClojureScript reagent example's
* Introduction
Reagent is a popular react wrapper in the clojurescript it greatly simplify build react SPA Applications this is usually a good starting point when learning, but there are lots of other options that are worth considering.
#+BEGIN_SRC html :results silent :exports none :tangle resources/public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Clojure demos</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/tachyons@4.12.0/css/tachyons.min.css">
</head>
<body>
<div id="app">
App loading here
</div>
<script src="/cljs-out/main_bundle.js" type="application/javascript"></script>
</body>
</html>
#+END_SRC
#+BEGIN_SRC edn :results silent :exports none :tangle deps.edn
{:paths ["src" "resources"]
:deps
{org.clojure/clojure {:mvn/version "1.10.0"}
org.clojure/clojurescript {:mvn/version "1.11.60"}
reagent/reagent {:mvn/version "1.2.0"}
thheller/shadow-cljs {:mvn/version "2.24.0"}}}
#+END_SRC
#+BEGIN_SRC edn :results silent :exports none :tangle shadow-cljs.edn
{:deps {:aliases [:dev]}
:dev-http {8080 ["resources/public/" "classpath:public"]}
:source ["src" "../../components"]
:builds {:app {:output-dir "resources/public/cljs-out/"
:asset-path "/cljs-out"
:target :browser
:compiler-options {:infer-externs :auto
:externs ["datascript/externs.js"]
:output-feature-set :es6}
:modules {:main_bundle {:init-fn clojure-demo.core/startup!}}
:devtools {:after-load app.main/reload!}}}}
#+END_SRC
#+BEGIN_SRC json :results silent :exports none :tangle package.json
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"shadow-cljs": "^2.23.3",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
}
}
#+END_SRC
#+BEGIN_SRC text :results silent :exports none :tangle readme.org
#+TITLE: Getting started
#+END_SRC
* Install the npm requirements.
npx install
* Launch shadow-cljs watch for source code changes
#+BEGIN_SRC sh
npx shadow-cljs watch app
#+END_SRC
#+BEGIN_SRC json :tangle :exports none src/clojure_demo/core.cljs
(ns clojure-demo.core
(:require
["react-dom/client" :refer [createRoot]]
[reagent.core :as reagent]))
#+END_SRC
* Components
The basis of any react app is components these are small snippets of html which can be composed to build up a page,
reagent provides 3 main ways to create components dependant on the complexity needed most of the time you will create form 1 and form 2 components.
** Form 1 components
Form one components are the most basic simply rendering some html with values that are not going to change.
In the example below the function just returns some hiccup with the parameters inserted, you need to specify =:key= when dynamically repeating the elements and these should be reproducible unique id's where possible not randomly generated or indexed numbers if the data is unordered.
*** Example navbar component
#+BEGIN_SRC clojure :tangle src/clojure_demo/core.cljs
(defn navbar-link [{:keys [href title text] :or {text nil title nil}}]
[:a.link.dim.dib.mr3.mb2.dark-blue {:key href :href href :title title} text])
(defn navbar []
[:div
[navbar-link {:href "https://clojure.org" :title "Clojure site" :text "Clojure"}]
[navbar-link {:href "https://github.com/reagent-project/reagent" :title "Reagent" :text "Reagent"}]
[navbar-link {:href "https://github.com/metosin/reitit" :title "Reitit" :text "Reitit"}]])
[navbar]
#+END_SRC
*** Example product cards
#+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs
(defn product-card
[{:keys [title amount description link]}]
[:article.br2.ba.dark-gray.b--black-10.ma2.w-100.w-50-m.w-25-l.mw5
[:img.db.w-100.br2.br--top {:src link}]
[:div.pa2.ph3-ns.pb3-ns
[:div.dt.w-100.mt1
[:div.dtc [:h1.f5.f4-ns.mv0 title]]
[:div.dtc.tr [:h2.f5.mv0 amount]]]
[:p.f6.lh-copy.measure.mt2.mid-gray description]]])
[:div.flex
[product-card
{:title "Cat 01"
:amount "£54.59"
:description "Cat 1 description here"
:link "http://placekitten.com/g/600/300"}]
[product-card
{:title "Cat 02"
:amount "£34.59"
:description "Cat 2 description here"
:link "http://placekitten.com/g/600/300"}]]
#+END_SRC
** Form 2 components
Form two components are used so we can track local state of a component, this is appropriate any time we need to react to change forms and user click event's being simple examples.
*** Example Click counter component
#+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs
(ns example
(:require
[reagent.core :as r]
[reagent.dom.server :as rdom]))
(defn my-component [title starting-value]
(let [local-state (reagent/atom starting-value)]
(fn [title]
[:h1 {:class (when @local-state "hide")
:on-click (fn [e]
(prn (-> e .-target))
(swap! local-state inc))}
(str title " " @local-state)])))
[my-component "Clickable component" 1]
#+END_SRC
*** Example address capture component
#+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs
(defn update-form-data [form-data ^js event]
(swap! form-data assoc
(keyword (-> event .-target .-name))
(-> event .-target .-value)))
(defn my-address-form []
(let [form-data (reagent/atom {})
form-change (partial update-form-data form-data)]
(fn []
[:div
(str @form-data)
[:form {:on-submit prn}
[:input.db.ma2.pa2 {:type "text"
:default-value (str (:test @form-data))
:name "address-line-1"
:on-change form-change :placeholder "Address Line 1"}]
[:input.db.ma2.pa2 {:type "text" :name "address-line-2" :on-change form-change :placeholder "Address Line 2"}]
[:input.db.ma2.pa2 {:name "address-line-3" :on-change form-change :placeholder "Address Line 3"}]
[:input.db.ma2.pa2 {:name "city" :on-change form-change :placeholder "City"}]
[:input.db.ma2.pa2 {:name "postcode" :on-change form-change :placeholder "Postcode / Zipcode"}]]])))
[:div [my-address-form]]
#+END_SRC
** Form 3 components
This form of component give's you full access to the react life cycle methods, so render did-mount did-unmount etc
usually this form of component is only needed when rendering graphics or things like graphs, it's also useful for capturing errors and handling them as in the example below, which renders your components but if =component-did-catch= is trigger the error is caught and displayed instead.
*** Error boundary example
If you hit an error react will stop rendering and remove the user interface, you can use an error boundary to capture this so part of the UI can still render.
#+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs
(defn err-boundary
[& children]
(let [err-state (reagent/atom nil)]
(reagent/create-class
{:display-name "ErrBoundary"
:component-did-catch (fn [err info]
(reset! err-state [err info]))
:reagent-render (fn [& children]
(if (nil? @err-state)
(into [:<>] children)
(let [[_ info] @err-state]
[:pre [:code (pr-str info)]])))})))
[err-boundary [:div ""]]
#+END_SRC
*** Example of using the google maps library
https://developers.google.com/maps/documentation/javascript/load-maps-js-api#dynamic-library-import
#+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs
(defn load-google-maps-script [api-key]
(let [script (.createElement js/document "script")]
;; copied from googles recommended way of loading google maps
(set! (.-innerHTML script) (str "(g=>{var h,a,k,p=\"The Google Maps JavaScript API\",c=\"google\",l=\"importLibrary\",q=\"__ib__\",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement(\"script\"));e.set(\"libraries\",[...r]+\"\");for(k in g)e.set(k.replace(/[A-Z]/g,t=>\"_\"+t[0].toLowerCase()),g[k]);e.set(\"callback\",c+\".maps.\"+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+\" could not load.\"));a.nonce=m.querySelector(\"script[nonce]\")?.nonce||\"\";m.head.append(a)}));d[l]?console.warn(p+\" only loads once. Ignoring:\",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
key: \"" api-key "\", v: \"weekly\"});"))
(.appendChild (.-head js/document) script)))
#+END_SRC
* Fetching a html element reference
If we wish to capture a node we can use =:ref= and store the result in an atom, we can then de reference the atom and call a method on the node using =aget=.
#+BEGIN_SRC clojurescript :results output :tangle src/clojure_demo/core.cljs
(defn example-ref-component [title]
(let [local-ref (reagent/atom nil)]
(fn [title]
[:div#example-ref-id.flex.items-center.justify-center.pa4.bg-lightest-blue.navy
{:ref #(reset! local-ref %)}
(str title (when @local-ref (aget @local-ref "id")))])))
[example-ref-component "Grabbing the element id using ref "]
#+END_SRC
#+BEGIN_SRC clojure :exports none :tangle src/clojure_demo/core.cljs
(defn current-page []
[:div
[navbar]
[:div.flex
[product-card
{:title "Cat 01"
:amount "£54.59"
:description "Cat 1 description here"
:link "http://placekitten.com/g/600/300"}]
[product-card
{:title "Cat 02"
:amount "£34.59"
:description "Cat 2 description here"
:link "http://placekitten.com/g/600/300"}]]
[my-component "Clickable component" 1]
[:div [my-address-form]]])
(defn mount-root-page []
;; this select the main node from the html file and injects your page content
(.render
(createRoot (.getElementById js/document "app"))
(reagent/as-element [err-boundary [current-page]])))
(def startup! (mount-root-page))
#+END_SRC
* Further reading
https://github.com/reagent-project/reagent
https://purelyfunctional.tv/guide/reagent/#what-is-reagent
https://github.com/metosin/reitit
https://www.metosin.fi/blog/reitit/

View File

@ -0,0 +1,44 @@
* Routing
Routing with reitit is all about data, you store your routes as nested vectors of hash maps.
the hash map should take a name and view param at least but you can add in params and validate the data.
Reitit works as a backend and frontend routing library so you can share routes between the two.
These are a few simple routes, the last takes parameters and does validation checking against the values.
#+BEGIN_SRC clojurescript
(def routes
[["/"
{:name ::frontpage
:view 'home-page-function}]
["/about"
{:name ::about
:view 'about-page-function}]
["/item/:id"
{:name ::item
:view 'item-page-function
:parameters {:path {:id int?}
:query {:foo keyword?}}}]])
#+END_SRC
You need to connect your routes data structure to =ref/start!= this function take's your own function where you can handle what should happen on route change, in this example an atom is updated causing react to render the new page.
#+BEGIN_SRC clojurescript
(def site-state (reagent/atom nil))
(rfe/start!
(rf/router routes {:data {:coercion rss/coercion}})
(fn [m] (swap! site-state assoc :current-route m))
;; set to false to enable HistoryAPI
{:use-fragment true})
#+END_SRC
To create a link to a route, you can use the =rfe/href= function which takes a lookup key which you specified in your routes, in this instance the key is name spaced to the current namespace.
#+BEGIN_SRC clojurescript
[:a {:href (rfe/href ::frontpage)} "example link"]
#+END_SRC

View File

@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, shrink-to-fit=no"
/>
<title>Clojure demos</title>
<meta
name="description"
content="Examples & Guides on using various libraries and technologies with in the clojure eco system's"
/>
<link rel="canonical" href="https://clojure-demos.digitaloctave.com/" />
<meta property="og:type" content="article" />
<meta property="og:title" content="TITLE OF YOUR POST OR PAGE" />
<meta property="og:description" content="DESCRIPTION OF PAGE CONTENT" />
<meta property="og:image" content="LINK TO THE IMAGE FILE" />
<meta property="og:url" content="PERMALINK" />
<meta property="og:site_name" content="SITE NAME" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"
integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<script src="https://cdn.tailwindcss.com"></script>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@100;200;300;400;500;600;700;800;900&family=Roboto&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
type="text/css"
href="https://raw.githubusercontent.com/FarhadG/code-mirror-themes/master/themes/rdark.css"
/>
<style>
html {
font-family: "Montserrat", Lato, Garamond;
}
.cm-s-rdark {
font-size: 1em;
line-height: 1.5em;
font-family: inconsolata, monospace;
letter-spacing: 0.3px;
word-spacing: 1px;
background: #1b2426;
color: #b9bdb6;
}
.cm-s-rdark .CodeMirror-lines {
padding: 8px 0;
}
.cm-s-rdark .CodeMirror-gutters {
box-shadow: 1px 0 2px 0 rgba(0, 0, 0, 0.5);
-webkit-box-shadow: 1px 0 2px 0 rgba(0, 0, 0, 0.5);
background-color: #1b2426;
padding-right: 10px;
z-index: 3;
border: none;
}
.cm-s-rdark div.CodeMirror-cursor {
border-left: 3px solid #b9bdb6;
}
.cm-s-rdark .CodeMirror-activeline-background {
background: #00000070;
}
.cm-s-rdark .CodeMirror-selected {
background: #e0e8ff66;
}
.cm-s-rdark .cm-comment {
color: #646763;
}
.cm-s-rdark .cm-string {
color: #5ce638;
}
.cm-s-rdark .cm-number {
color: null;
}
.cm-s-rdark .cm-atom {
color: null;
}
.cm-s-rdark .cm-keyword {
color: #5ba1cf;
}
.cm-s-rdark .cm-variable {
color: #ffaa3e;
}
.cm-s-rdark .cm-def {
color: #ffffff;
}
.cm-s-rdark .cm-variable-2 {
color: #ffffff;
}
.cm-s-rdark .cm-property {
color: null;
}
.cm-s-rdark .cm-operator {
color: #5ba1cf;
}
.cm-s-rdark .CodeMirror-linenumber {
color: #646763;
}
</style>
<meta name="robots" content="all" />
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-59HNPDZF2T"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-59HNPDZF2T");
</script>
</head>
<body>
<div id="app">loading here</div>
<script
src="/cljs-out/main_bundle.js"
type="application/javascript"
></script>
</body>
</html>

View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, shrink-to-fit=no"
/>
<title>Clojure demos</title>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<meta name="theme-color" content="#ffffff" />
<link
rel="stylesheet"
type="text/css"
href="https://storage.googleapis.com/app.klipse.tech/css/codemirror.css"
/>
<link
rel="stylesheet"
type="text/css"
href="https://unpkg.com/tachyons@4.12.0/css/tachyons.min.css"
/>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@100;200;300;400;500;600;700;800;900&family=Roboto&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
type="text/css"
href="https://raw.githubusercontent.com/FarhadG/code-mirror-themes/master/themes/rdark.css"
/>
<style>
html {
font-family: "Montserrat", Lato, Garamond;
}
#notification-container {
position: fixed;
z-index: 999999;
top: 12px;
right: 12px;
</style>
<meta name="robots" content="all" />
</head>
<body>
<div id="app">loading here</div>
<script src="/cljs-out/main_bundle.js" type="text/javascript"></script>
</body>
</html>

View File

@ -0,0 +1,25 @@
(ns com.oly.static-sites.clojure-demos.components
(:require
[com.oly.static-sites.clojure-demos.state :refer [site-state]]
[com.oly.static-sites.clojure-demos.helpers :refer [copy->clipboard slugify]]))
(defn notification []
(if (-> @site-state :notification)
[:div.fixed.p-5.top-20.right-0.border-2.border-sky-400.bg-white.opacity-100.transition-opacity.delay-150.duration-200
(-> @site-state :notification)]
[:<>]))
(defn header-on-click-copy-link [v]
{:on-click (fn [] (copy->clipboard (slugify v)))})
(defn header-lvl1 [v _]
[:h1.text-2xl.fw6.lh-title.mt-0.mb-2 {:id (slugify v)}
[:a {:name (slugify v)} [:i.fa-solid.fa-link (header-on-click-copy-link v)] v]])
(defn header-lvl2 [v _]
[:h2.text-xl.fw6.lh-title.mt-0.mb-2 {:id (slugify v)}
[:a {:name (slugify v)} [:i.fa-solid.fa-link (header-on-click-copy-link v)] v]])
(defn header-lvl3 [v _]
[:h3.text-base.fw6.lh-title.mt-0.mb-2 {:id (slugify v)}
[:a {:name (slugify v)} [:i.fa-solid.fa-link (header-on-click-copy-link v)] v]])

View File

@ -0,0 +1,446 @@
(ns com.oly.static-sites.core
(:require
["@codemirror/language" :refer [LanguageSupport StreamLanguage]]
["@codemirror/legacy-modes/mode/yaml" :refer [yaml]]
["@codemirror/state" :as cm-state :refer [EditorState Transaction]]
["@codemirror/theme-one-dark" :refer [oneDark]]
["@codemirror/view" :as cm-view :refer [EditorView ViewUpdate]]
["@nextjournal/lang-clojure" :refer [clojure]]
["react-dom/client" :refer [createRoot]]
["tarts" :as tarts]
[ajax.core :refer [GET raw-response-format]]
[cl-eorg.html :as h :refer [body headers org->replacements]]
[cl-eorg.parser :as o :refer [parse]]
[cl-eorg.themes.tachyon :refer [tachyon-theme]]
[com.oly.static-sites.components
:refer [header-lvl1 header-lvl2 header-lvl3 notification]]
[com.oly.static-sites.helpers :refer [slugify]]
[com.oly.static-sites.state :refer [site-state]]
[com.oly.static-sites.tailwind-theme :refer [tailwind-theme]]
[clojure.data.xml :as xml]
[clojure.string :as str]
[honey.sql :as sql]
[promesa.core :as promesa]
[honey.sql.helpers :as sqlh]
[reagent.core :as reagent]
[reitit.coercion.spec :as rss]
[reitit.frontend :as rf]
[reitit.frontend.easy :as rfe]
[reitit.frontend.history :refer [ignore-anchor-click?]]
[sci.core :as sci]
[sci.configs.funcool.promesa :as sci-promesa]
[sci.configs.reagent.reagent :as sci-reagent]
[sci.configs.tonsky.datascript :as sci-datascript]
[com.oly.static-sites.routes :refer [site-data routes]]
[com.oly.static-sites.sitemap :as sm #_#_:refer [spit-pwa-sitemap]]
[tick.core :as t]
(com.oly.static-sites.sci-init :refer [sci-ctx yaml-mode]))
#_(:require-macros [com.oly.static-sites.sitemap :as sm #_#_:refer [spit-pwa-sitemap]]))
(defn fetch-selected-text
"Get the users selected text"
[updated-view transactions]
(reduce (fn [text t]
(if (= "select.pointer" (str (.annotation t (.-userEvent Transaction))))
(conj text
(.sliceDoc
^EditorState (.-state updated-view)
(.-from (.-main (.-selection t)))
(.-to (.-main (.-selection t)))))
text))
[]
transactions))
(defn err-boundary
"https://github.com/reagent-project/reagent/blob/master/doc/ReactFeatures.md#error-boundaries"
[children]
(let [err-state (reagent/atom nil)]
(reagent/create-class
{:display-name "Error Boundary"
:get-derived-state-from-error (fn [e]
(reset! err-state e #_[err info])
#js {})
:reagent-render (fn [children]
(if @err-state
[:div (str @err-state)] children))})))
(defn code-editor
[{:keys [exports class results]} content]
(let [language class
editor (atom nil)
evaled-result (reagent/atom nil)
update-timeout (reagent/atom nil)
update-fn (.of (.-updateListener EditorView)
(fn [^ViewUpdate view-update]
(when (and (not= results "none")
(some #(= language %) ["clojure" "clojurescript"]))
(js/clearTimeout @update-timeout)
(reset! update-timeout
(js/setTimeout
(fn []
(let [selected-text (fetch-selected-text view-update (.-transactions view-update))
eval-code (if (seq selected-text)
(first selected-text)
(.-doc (.-state view-update)))]
(reset! evaled-result (sci/eval-string* sci-ctx
(.toString eval-code)))
(prn "updated delayed"))) 1000)))))
start-state
(.create EditorState
(clj->js {:doc content
:mode "text/x-clojure"
;:mode "text/yaml"
:extensions [(clojure) yaml-mode update-fn oneDark #_cm-keymap/of #_cm-commands/default-keymap]}))
view (atom nil)]
(reagent/create-class
{:component-did-mount
(fn [_]
(reset! view (EditorView.
(clj->js (merge {} #_(get languages language)
{:state start-state
;:mode "yaml"
:mode "clojure"
:updateListener prn
:parent @editor})))))
:component-will-unmount (fn [_] (.destroy @view))
:reagent-render (fn []
(if (= exports "none")
nil
[:div.mb-8
[:div.ba.ma-0.f5.b--black-05.pa2.overflow-auto.editor {:ref #(reset! editor %)}]
(when @evaled-result
[err-boundary
[:pre.border-2.pa-4.pl-6.text-xl
{:style {:white-space "pre-line"}}
;; See org mode :results key
(case results
"verbatim" (str @evaled-result)
"value" (str @evaled-result)
"output" @evaled-result
"silent" ""
@evaled-result)]])]))})))
(defn link-handler
"Should use youtube.com/embed for embeds"
[v _]
(cond
(some? (re-matches #"^(https://youtu.be/|https://www.youtube.com/).*" (str (:href v))))
[:iframe {:src (:href v)
:title "YouTube video player"
:width "560"
:height "315"
:frameborder "0"
:allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"}]
:else [:a {:href (:href v)} (str v)]))
(def theme-tachyon
(merge tachyon-theme
{:SRC code-editor
:HEADER1 header-lvl1 #_(fn [v _] [:h1 {:id (slugify v)} [:a {:name (slugify v)} v]])
:HEADER2 header-lvl2 #_(fn [v _] [:h2 {:id (slugify v)} [:a {:name (slugify v)} v]])
:HEADER3 header-lvl3 #_(fn [v _] [:h2 {:id (slugify v)} [:a {:name (slugify v)} v]])
:LINE :p.f5.f5-ns.lh-copy.mt0
:BULLETS :ul.mt0
:LINK :a #_link-handler}))
(def theme
(merge tailwind-theme
{:SRC code-editor
:HEADER1 header-lvl1 #_(fn [v _] [:h1.text-2xl.fw6.lh-title.mt-0.mb-2 {:id (slugify v)} [:a {:name (slugify v)} v]])
:HEADER2 header-lvl2 #_(fn [v _] [:h2.text-xl.fw6.lh-title.mt-0.mb-2 {:id (slugify v)} [:a {:name (slugify v)} v]])
:HEADER3 header-lvl3 #_(fn [v _] [:h2.text-base.lh-title.mt-0.mb-2 {:id (slugify v)} [:a {:name (slugify v)} v]])
:LINE :p.f5.f5-ns.lh-copy.mt0
:BULLETS :ul.mt0
:LINK link-handler}))
(def theme-toc-tachyon
(merge tachyon-theme
{:HEADER1 (fn [v _] [:li [:a {:href (str "#" (slugify v))}
[:span.f4.fw6.f3-ns.lh-title.mt0.mb2 v]]])
:HEADER2 (fn [v _] [:li.ml2 [:a {:href (str "#" (slugify v))}
[:span.f4.fw6.f4-ns.lh-title.mt0.mb2 v]]])
:HEADER3 (fn [v _] [:li.ml4 [:a {:href (str "#" (slugify v))}
[:span.f5.fw6.f5-ns.lh-title.mt0.mb2 v]]])
:HEADER4 (fn [v _] [:li.ml6 [:a {:href (str "#" (slugify v))}
[:span.f6.fw6.f5-ns.lh-title.mt0.mb2 v]]])
:HEADER5 (fn [v _] [:li.ml8 [:a {:href (str "#" (slugify v))}
[:span.f6.fw6.f5-ns.lh-title.mt0.mb2 v]]])
:HEADER6 (fn [v _] [:li.ml10 [:a {:href (str "#" (slugify v))}
[:span.f6.fw6.f5-ns.lh-title.mt0.mb2 v]]])}))
(def theme-toc
(merge tailwind-theme
{:HEADER1 (fn [v _] [:li.list-decimal [:a.text-blue-800 {:class "hover:text-blue-500" :href (str "#" (slugify v))}
[:span.text-xl.fw6.lh-title.mt0.mb2 v]]])
:HEADER2 (fn [v _] [:li.list-decimal.ml-4 [:a.text-blue-800 {:href (str "#" (slugify v))}
[:span.text-lg.fw6.lh-title.mt0.mb2 v]]])
:HEADER3 (fn [v _] [:li.list-decimal.ml-8 [:a.text-blue-800 {:class "hover:text-blue-500" :href (str "#" (slugify v))}
[:span.text-base.fw6.lh-title.mt0.mb2 v]]])
:HEADER4 (fn [v _] [:li.list-decimal.ml-12 [:a.text-blue-800 {:href (str "#" (slugify v))}
[:span.text-sm.fw6.lh-title.mt0.mb2 v]]])
:HEADER5 (fn [v _] [:li.list-decimal.ml-26 [:a.text-blue-800 {:href (str "#" (slugify v))}
[:span.text-xs.fw6.f5-ns.lh-title.mt0.mb2 v]]])
:HEADER6 (fn [v _] [:li.list-decimal.ml-30 [:a.hover:decoration-blue-400 {:href (str "#" (slugify v))}
[:span.text-xs.fw6.f5-ns.lh-title.mt0.mb2 v]]])}))
;; form one component to render an article
(defn article [{:keys [title description tagline]}]
[:article.prose {:data-name "article-full-bleed-background"}
[:div.cf {:style {:background "url(http://placekitten.com/g/600/300)"
:no-repeat "center center fixed" :background-size "cover"}}
[:div.fl.pa3.pa4-ns.bg-white.black-70.measure-narrow.f3.times
[:header.b--black-70.pv4 {:class (when tagline "bb")}
[:h3.f2.fw7.ttu.tracked.lh-title.mt0.mb3.avenir title]
(when tagline [:h4.f3.fw4.i.lh-title.mt0 tagline])]
[:section.pt5.pb4 [:p.times.lh-copy.measure.f5.mt0 description]]]]])
;; form one component to render article tiles
(defn articles [{:keys [title body articles]}]
[:section.mw7.center.avenir
[:h2.text-4xl.mt-4.mb-4 title]
(map (fn [{:keys [title author link description img-src img-alt] :as article}]
[:article.bt.bb.b--black-10.border-b-2 {:key title}
[:a {:href link}
[:div.flex.flex-column.flex-row-ns.p-4
[:div.flex-none.w-32
(when img-src
[:img.w-32 #_h-24.block {:src img-src :alt img-alt}])]
[:div.ml-4.flex-grow #_{:class "w-2/3"}
[:h1.text-2xl title]
[:p description]
[:p author]]]]])
articles)])
;; form one component to render a nav link
(defn navbar-link [{:keys [href title text key] :or {text nil title nil}}]
[:a.link.m-2 {:key (or key href) :class "hover:text-grey-200" :href href :title title} text])
;; form one component to render a navbar
(defn navbar [links]
[:header.text-white.bg-zinc-900.p-6.pl-12.w-full
(into [:nav.uppercase.font-medium.text-sm] (mapv navbar-link links))]
#_[:header.bg-black-90.w-100.ph3.pv3.pv4-ns.ph4-m.ph5-l
(into [:nav.f6.fw6.ttu.tracked] (mapv navbar-link links))])
(defn footer []
[:footer.bg-zinc-800.text-slate-400.p-8.m-auto.w-full
[:div.m-auto {:class "w-4/5"}
[:a.hover:text-slate-200
{:target "_blank" :href "https://matrix.to/#/@oly:matrix.org"}
"Contact me"]]]
#_[:div
[:footer.bg-near-black.white-80.pv4.ph3.ph5-m.ph6-l.mid-gray.w-full
[:a.f6.dib.ph2.link.mid-gray.dim
{:target "_blank" :href "https://matrix.to/#/@oly:matrix.org"}
"Contact me"]]])
(def toc (partial contains? (into #{} (map keyword [:HEADER1 :HEADER2 :HEADER3 :HEADER4 :HEADER5 :HEADER6]))))
(def org-code (partial contains? (into #{} (map keyword [:SRC]))))
(defn build-file-tar [code-blocks]
(tarts (clj->js (mapv (fn [block] {:name (str (:tangle (second block)))
:content (str (last block))})
code-blocks))))
(defn build-file-tar-hm [code-blocks]
(tarts (clj->js code-blocks)))
(defn build-tarts-map [blocks]
(->> blocks
(group-by (fn [block] (:tangle (second block))))
(reduce (fn [m [k v]]
(conj m {:name (str k)
:content (str (str/join "\n\n" (mapv last v)))})) [])))
(defn org->split2
"Split out meta and body"
[dsl]
{:header (filter (fn filter-headers [tag] (headers (first tag))) dsl)
:toc (filter (fn filter-toc [tag] (toc (first tag))) dsl)
:code (filter (fn filter-code [tag] (org-code (first tag))) dsl)
:body (filter (fn filter-body [tag] (body (first tag))) dsl)})
(defn home-page []
[:<>
[articles
{:title "Clojure Demos"
:body (-> site-data :homepage :intro)
:articles
[{:title "Clojure Basics"
:description "Getting started with clojure syntax datatype's sequences conditions"
:link (rfe/href :page {:page "clojure-basics"})
:img-src "https://clojure.org/images/clojure-logo-120b.png"}
{:title "Reagent Demo"
:description "React application using reagent"
:link (rfe/href :page {:page "reagent-demo"})
:img-src "https://raw.githubusercontent.com/reagent-project/reagent/master/logo/logo-text.png"}]}]])
(defn grouped-list [data]
[articles
{:title (-> data :title)
:body (-> data :intro)
:articles
(mapv (fn fmt-map [demo]
{:title (:title demo)
:description (:description demo)
:link (rfe/href :page {:page (:page demo)})
:img-src (:icon-image demo)})
(-> data :demos))}])
(defn grouped-page [route]
(let [group (keyword (name (:name (:data route))))]
(if (vector? (-> site-data :pages group ))
(into [:div]
(mapv #(grouped-list %) (-> site-data :pages group)))
(grouped-list (-> site-data :pages group )))
#_[:<> [articles
{:title (-> site-data :pages group :title)
:body (-> site-data :pages group :intro)
:articles
(mapv (fn fmt-map [demo]
{:title (:title demo)
:description (:description demo)
:link (rfe/href :page {:page (:page demo)})
:img-src (:icon-image demo)})
(-> site-data :pages group :demos))}]]))
(defn default-page [route]
(let [demo-key (keyword (-> route :parameters :path :page))
content (reagent/atom {})]
(GET (str "/" (-> site-data :demos demo-key :file))
{:response-format (raw-response-format)
:handler (fn [response]
(->> response
parse
org->split2
(reset! content)))})
(fn [route]
(if @content
[:main
[:h1.mt-8.mb-8.text-4xl (:content (last (first (:header @content))))]
[:div
(into [:div] (org->replacements theme (:body @content)))]]
[:<>]))))
(defn default-page-header [route]
(let [demo-key (keyword (-> route :parameters :path :page name))
org-file (-> site-data :demos demo-key :file)
content (reagent/atom {})]
(when org-file
(GET (str "/" org-file)
{:response-format (raw-response-format)
:handler (fn [response]
(->> response
parse
org->split2
(reset! content)))}))
(fn [route]
(if @content
[:main
[:h1.mt-8.mb-8.text-4xl (:content (last (first (:header @content))))]
[:div.mw7.center.avenir
(into [:ol.list-inside.m-6.font-semibold] (org->replacements theme-toc (:toc @content)))]
[:p "The code in these examples is evaluated when modified, you can highlight a partial expression to evalute the selection, You can also download the code as a tar if you like and use it in your favourite editor."]
[:a.bg-sky-500.text-white.m-2.p-2.pl-4.pr-4.inline-block
{:download (str (slugify (:content (last (first (:header @content))))) ".tar")
:title (:content (last (first (:header @content))))
:href (.createObjectURL
js/URL
(js/Blob. #js [(build-file-tar-hm
(build-tarts-map
(:code @content)))]
{:type "application/tar"}))}
"Download Code"]
[:div
(into [:div] (org->replacements theme (:body @content)))]]
[:<> "Sorry page not found"]))))
;; form one render about page component
(defn about-page []
[default-page {:parameters {:path {:page "about"}}}])
(defn render-view
"We lookup the view to render from a key in the route, this keeps our routes as pure data"
[view]
(case view
:homepage home-page
:grouped-page grouped-page
:default-page-header default-page-header
:about-page about-page
grouped-page))
;; top level component contains nav and adds in the select page into a containing element
;; we are adding in a style sheet but this will often be done in index.html
(defn current-page []
(let [route (reagent/cursor site-state [:current-route])]
(swap! site-state dissoc :notification)
(fn []
[:div
[notification]
[navbar (concat
[{:href (rfe/href :frontpage) :title "title here" :text "home" :key "homepage"}]
(mapv (fn build-nav [[_ page]]
{:href (rfe/href (:key page))
:text (:title page)})
(-> site-data :pages))
[{:title "About page" :href (rfe/href :about) :text "About" :key "about"}
#_{:href (rfe/href ::i-do-not-exist) :text "missing"}])]
[:main.m-auto {:class "w-3/5 md:w-4/5"}
(when-let [view (-> @route :data :view render-view)]
[:div
[view @route]])]
[footer]])))
;; This simply calls reagent render and puts the result in a div with the id of app
;; you can create your own index.html or figwheel provides one with the app id which will replace the default data
;; ^:after-load is meta data its not needed but informs figwheel to run this code after a page load
(defn mount-root-page []
;; this select the main node from the html file and injects your page content
(.render
(createRoot (.getElementById js/document "app"))
(reagent/as-element [err-boundary [current-page]])))
(defn ^:after-load render-site []
;; this select the main node from the html file and injects your page content
(mount-root-page))
(defn ^:dev/after-load startup! []
(rfe/start!
(rf/router routes {:data {:coercion rss/coercion}})
(fn [m] (swap! site-state assoc :current-route m))
;; set to false to enable HistoryAPI
{:use-fragment false
:ignore-anchor-click?
(fn [router e el uri]
;; Add additional check on top of the default checks\
(and
(ignore-anchor-click? router e el uri)
(not (let [href (or (.-href el) "")
result (str/includes? href "#")]
#_(when result
;(.preventDefault e)
#_(js/console.log "will prevent by href" href))
result))))})
(render-site))
;; Generate the sitemap at start, routes are stored as plain data in cljc files
;; this means cljs and clj can read and manipulate them
(sm/spit-pwa-sitemap
"resources/public/sitemap.xml"
"https://clojure-demos.digitaloctave.com")
;; we defonce the startup so that hot reloading does not reinitialize the state of the site
(def launch (do (startup!) true))
(comment
@site-state
(org->replacements tachyon-theme [[:SRC {:LANGUAGE "shell"} "hi"]])
(GET "/test.org" {:handler (fn [response] (swap! site-state assoc :content response))}))

View File

@ -0,0 +1,46 @@
(ns com.oly.static-sites.clojure-demos.helpers
(:require
[cljs.core.async :refer [go]]
[cljs.core.async.interop :refer [<p!]]
[com.oly.static-sites.clojure-demos.state :refer [site-state]]
[clojure.string :as str]))
(defn slugify [s]
(when s
(str
(-> s
(str/lower-case)
(str/replace #"[^\w]+" "-")
(str/replace #"^-\\|-\\-$" "")))))
(defn build-page-path [title]
(str "http://127.0.0.1:8080/" #_"https://clojure-demos.digitaloctave.com/" "page/"
(when (-> @site-state :current-route :parameters :path :page)
(-> @site-state :current-route :parameters :path :page))
"#" title))
(defn copy->clipboard
"Simple wrapper which copies text to the clipboard and resolves the promise"
[text]
(go (<p! (.writeText (.-clipboard js/navigator) (build-page-path text)))
(swap! site-state assoc :notification "Copied to clipboard")
(prn "copied")))
(defn map-replace
"Given a string with {:key} strings substitute the matching key in a hash map"
[text m]
(reduce
(fn [acc [k v]] (str/replace acc (str "{" k "}") (str v)))
text m))
(comment
(map-replace "/page/{page}" "page")
(re-seq #"\{(.*?)\}" "/page/{page}/{id}" )
(->> (re-seq #"\{(.*?)\}" "/page/{page}/{id}" )
(map second))
;(re-seq #"{(.*?)}" "/page/{page}" )
(re-seq #"\[(.*?)\]" "/page/[page]" )
(.-writeText (.-clipboard js/navigator) "test")
(copy->clipboard "test2"))

View File

@ -0,0 +1,5 @@
(ns com.oly.static-sites.interface
(:require [com.oly.static-sites.routes :as routes]))
(def routes routes/routes)
(def site-data routes/site-data)

View File

@ -0,0 +1,137 @@
(ns com.oly.static-sites.clojure-demos.routes
(:require [spec-tools.data-spec :as ds]))
;; put constant data here
(def site-data
{:homepage {:intro "Clojure tutorials examples and exploration"}
:dslpage {:intro "A domain-specific language (DSL) is a language designed to be used for a specific task or domain, clojure has a rich set of DSL some popular DSL's are listed on this page with example's on usage. "}
:lorem "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
:pages {:dialects {:title "Dialects & Interop"
:intro "Clojure has the ability to run on multiple technology stacks, this allows code reuse and consistency regardless of which eco system you want to make use of."
:key :dialects
:demos [{:title "Clojure"
:description "The original language running on the jvm with access to the java eco system"
:icon-image "https://clojure.org/images/clojure-logo-120b.png"
:page "clojure-basics"}
{:title "Clojurescript"
:description "Clojure running in the browser or on top of node, with access to the js eco system."
:icon-image "https://clojurescript.org/images/cljs-logo-60b.png"
:page "clojurescript-basics"}
{:title "ClojureCLR"
:description "Clojure running on microsofts CLR with access to .net ecosystem"
:icon-image "https://clojure.org/images/clojure-logo-120b.png"
:page "clojureclr-basics"}
{:title "ClojureDart"
:description "Clojure for dart, use clojure to build apps using flutter"
:page "clojuredart-info"}]}
:dsl {:title "DSL's"
:key :dsl
:intro "A domain-specific language (DSL) is a language designed to be used for a specific task or domain, clojure has a rich set of DSL some popular DSL's are listed on this page with example's on usage. "
:demos [{:title "Hiccup HTML Demo"
:page "hiccup-dsl-demo"
:description "Hiccup is a DSL for generating HTML it uses data structures over strings to generate html, this makes it far easier to dynamically generate html by using the language constructs to manipulate the tree."
; :link (rfe/href ::demo {:page "hiccup-dsl-demo"})
:icon-image "https://miro.medium.com/max/1400/1*CEYFj5R57UFyCXts2nsBqA.png"}
{:title "Honey SQL Demo"
:page "honey-sql-demo"
:description "Similar to honey but for SQL allows you to split apart your complex queries into parts and compose them together."
; :link (rfe/href ::demo {:page "honey-dsl-demo"})
:icon-image "https://miro.medium.com/max/1400/1*CEYFj5R57UFyCXts2nsBqA.png"}
{:title "Datalog Demo"
:page "datalog-demo"
:description "Datalog is a popular DSL with in the clojure for querying deductive database system's"
; :link (rfe/href ::demo {:page "datalog-demo"})
:icon-image "https://raw.githubusercontent.com/tonsky/datascript/master/extras/logo.svg"}]}
:devops {:title "Deployment & testing"
:intro "Running, Testing and deploying your software in a CI pipeline"
:key :devops
:demos [{:title "CI Demo"
:description "Clojure in a CI pipeline, building artifacts running tests & deploying"
:page "ci-demo"
;;:link (rfe/href ::demo {:page "ci-demo"})
:icon-image "https://avatars.githubusercontent.com/u/2181346?s=200&v=4"}]}
:examples {:title "Examples"
:intro "Some example applications"
:key :examples
:demos [{:title "Maps"
:description "Some quick examples using frontend map api's like google maps."
:page "example-maps"
:icon-image "https://avatars.githubusercontent.com/u/2181346?s=200&v=4"}
{:title "XML sitemap"
:description "Using data.xml to generate a sitemap"
:page "example-xml-sitemap"
:icon-image "https://avatars.githubusercontent.com/u/2181346?s=200&v=4"}]}
#_#_:terminology {:title "Terminology"
:intro ""
:key :terminology}}
:demos
{:hiccup-dsl-demo
{:file "documents/hiccup-dsl-demo.org" :git-link "https://github.com/atomjuice/dsl-demo"}
:datalog-demo
{:file "documents/datalog-demo.org" :git-link "https://github.com/atomjuice/dsl-demo"}
:honey-sql-demo
{:file "documents/honey-sql-demo.org" :git-link "https://github.com/atomjuice/dsl-demo"}
:reagent-demo
{:file "documents/reagent-reitit.org" :git-link "https://github.com/atomjuice/dsl-demo"}
:clojure-basics
{:file "documents/clojure-basics.org" :git-link "https://github.com/atomjuice/dsl-demo"}
:ci-demo
{:file "documents/ci-demo.org" :git-link "https://github.com/atomjuice/dsl-demo"}
:example-maps
{:file "documents/examples-maps.org" :git-link ""}
:example-xml-sitemap
{:file "documents/examples-xml-sitemap.org" :git-link "https://github.com/atomjuice/dsl-demo"}
:containers
{:file "documents/containers.org" :git-link "https://github.com/atomjuice/containers"}
:about
{:file "documents/about.org" :git-link "https://github.com/atomjuice/containers"}}})
(defn get-pages
"Get pages from site-data map"
[]
(->> site-data
:pages
(map second)
(map :demos)
(map (fn cat [c] (map (fn build-url [page] (str "/page/" (:page page))) c)))
flatten))
;; define our page routes passed into reitit later on
(def routes
[["/"
{:name :frontpage
:view :homepage}]
["/page/{page}"
{:name :page
:sitemap {:vec get-pages}
:view :default-page-header
:parameters {:path {:page string?}
:query {(ds/opt :foo) keyword?}}}]
["/terminology/"
{:name :terminology
:view :grouped-page}]
["/dialects/"
{:name :dialects
:view :grouped-page}]
["/examples/"
{:name :examples
:view :grouped-page}]
["/devops/"
{:name :devops
:view :grouped-page}]
["/dsl/"
{:name :dsl
:view :grouped-page}]
["/about/"
{:name :about
:view :about-page}]])

View File

@ -0,0 +1,124 @@
(ns com.oly.static-sites.clojure-demos.sci-init
(:require
["@codemirror/language" :refer [LanguageSupport StreamLanguage]]
["@codemirror/legacy-modes/mode/yaml" :refer [yaml]]
["@codemirror/state" :as cm-state :refer [EditorState Transaction]]
["@codemirror/theme-one-dark" :refer [oneDark]]
["@codemirror/view" :as cm-view :refer [EditorView ViewUpdate]]
["@nextjournal/lang-clojure" :refer [clojure]]
["react-dom/client" :refer [createRoot]]
["tarts" :as tarts]
[ajax.core :refer [GET raw-response-format]]
[cl-eorg.html :as h :refer [body headers org->replacements]]
[cl-eorg.parser :as o :refer [parse]]
[cl-eorg.themes.tachyon :refer [tachyon-theme]]
[com.oly.static-sites.clojure-demos.components
:refer [header-lvl1 header-lvl2 header-lvl3 notification]]
[com.oly.static-sites.clojure-demos.helpers :refer [slugify]]
[com.oly.static-sites.clojure-demos.state :refer [site-state]]
[com.oly.static-sites.clojure-demos.tailwind-theme :refer [tailwind-theme]]
[clojure.data.xml :as xml]
[clojure.string :as str]
[honey.sql :as sql]
[promesa.core :as promesa]
[honey.sql.helpers :as sqlh]
[reagent.core :as reagent]
[reitit.coercion.spec :as rss]
[reitit.frontend :as rf]
[reitit.frontend.easy :as rfe]
[reitit.frontend.history :refer [ignore-anchor-click?]]
[sci.core :as sci]
[sci.configs.funcool.promesa :as sci-promesa]
[sci.configs.reagent.reagent :as sci-reagent]
[sci.configs.tonsky.datascript :as sci-datascript]
[com.oly.static-sites.clojure-demos.routes :refer [site-data routes]]
[com.oly.static-sites.clojure-demos.sitemap :as sm #_#_:refer [spit-pwa-sitemap]]
[tick.core :as t]))
(def languages
{"clojure" {:mode "clojure"}
"clojurescript" {:mode "clojure"}
"html" {:mode "html"}
"json" {:mode "json"}
"yaml" {:mode "yaml"}})
(def yaml-mode (LanguageSupport. (.define StreamLanguage yaml)))
(defn ^:sci/macro my-js-await
[_ _ [name thenable] & body]
(let [last-expr (last body)
[body catch]
(if (and (seq? last-expr) (= 'catch (first last-expr)))
[(butlast body) last-expr]
[body nil])]
;; FIXME: -> here will always return a promise so shouldn't be necessary to add js hint?
`(-> ~thenable
~@(when (seq body)
[`(.then (fn [~name] ~@body))])
~@(when catch
(let [[name & body] catch]
[`(.catch (fn [~name] ~@body))])))))
;; https://github.com/babashka/sci.configs
(def rf-ns (sci/create-ns 'reitit.frontend nil))
(def rfe-ns (sci/create-ns 'reitit.frontend.easy nil))
(def rss-ns (sci/create-ns 'reitit.coercion.spec nil))
(def sql-ns (sci/create-ns 'honey.sql.core nil))
(def sqlh-ns (sci/create-ns 'honey.sql.helpers nil))
(def shadow-ns (sci/create-ns 'shadow.cljs.modern nil))
(def xml-ns (sci/create-ns 'clojure.data.xml nil))
(def tick-ns (sci/create-ns 'tick.core nil))
;; core async does not work with sci
(def async-ns (sci/create-ns 'cljs.core.async nil))
(def async-in-ns (sci/create-ns 'cljs.core.async.interop nil))
;; https://github.com/babashka/sci.configs
(def sci-ctx
(sci/init
{:classes {'js js/globalThis :allow :all}
:features #{:cljs}
:aliases {'promesa 'promesa.core
'reagent 'reagent.core
'd 'datascript.core}
:namespaces
{'reagent.core sci-reagent/reagent-namespace
'reagent.ratom sci-reagent/reagent-ratom-namespace
;'reagent.dom.server sci-reagent-server/namespaces
'promesa.core sci-promesa/promesa-namespace
'datascript.core sci-datascript/core-namespace
'datascript.db sci-datascript/db-namespace
;'h {'html (sci/copy-var h/html hc-ns)}
;'async {'go (sci/copy-var async/go async-ns)}
;'shadow {'js-await (sci/copy-var js-await shadow-ns)}
'shadow {'js-await (sci/copy-var my-js-await shadow-ns)}
;'async-in {'<p! (sci/copy-var async-in/<p! async-in-ns)}
'tick {'format (sci/copy-var t/format tick-ns)
'zoned-date-time (sci/copy-var t/zoned-date-time tick-ns)}
'xml {'emit-str (sci/copy-var xml/emit-str xml-ns)
;; dont think indent-str & alias uri is supported in clojurescript
;;'indent-str (sci/copy-var xml/indent-str xml-ns)
;;'alias-uri (sci/copy-var xml/alias-uri xml-ns)
}
'sql {'format (sci/copy-var sql/format sql-ns)
#_#_'raw (sci/copy-var sql/raw sql-ns)}
'sqlh {'select (sci/copy-var sqlh/select sqlh-ns)
'from (sci/copy-var sqlh/from sqlh-ns)
'limit (sci/copy-var sqlh/limit sqlh-ns)
'join (sci/copy-var sqlh/join sqlh-ns)
'values (sci/copy-var sqlh/values sqlh-ns)
'on-conflict (sci/copy-var sqlh/on-conflict sqlh-ns)
'do-update-set (sci/copy-var sqlh/do-update-set sqlh-ns)
'insert-into (sci/copy-var sqlh/insert-into sqlh-ns)
'order-by (sci/copy-var sqlh/order-by sqlh-ns)
'group-by (sci/copy-var sqlh/group-by sqlh-ns)
'where (sci/copy-var sqlh/where sqlh-ns)}
'rf {'router (sci/copy-var rf/router rf-ns)}
'rss {'coercion (sci/copy-var rss/coercion rss-ns)}
'rfe {'start! (sci/copy-var rfe/start! rfe-ns)
'href (sci/copy-var rfe/href rfe-ns)}}}))
;(def sci-ctx (sci/empty-environment))
(sci/alter-var-root sci/print-fn (constantly *print-fn*))

View File

@ -0,0 +1,36 @@
(ns com.oly.static-sites.clojure-demos.sitemap
(:require [clojure.data.xml :as xml]
[com.oly.static-sites.clojure-demos.routes :refer [routes]]))
;; Warning here be macro magic to build the sitemap.
(defn pwa-sitemap [domain routes]
(xml/indent-str
{:tag :urlset
:attrs {:xmlns "http://www.sitemaps.org/schemas/sitemap/0.9"}
:content
(mapv (fn [r]
{:tag :url
:attrs {}
:content [{:tag :loc :attrs {} :content [(str domain r)]}]}
) routes)}))
(defn build-sitemap-urls [routes]
(->> routes
(reduce (fn handle-sitemap [result [route route-map]]
(if (-> route-map :sitemap :vec)
(conj result ((-> route-map :sitemap :vec)))
(conj result route)
)) [])
flatten))
(defmacro spit-pwa-sitemap
"Generate the sitemap and outut to the provided path"
[path domain ]
(clojure.core/spit
path
(pwa-sitemap
domain
(build-sitemap-urls routes))))

View File

@ -0,0 +1,13 @@
(ns com.oly.static-sites.clojure-demos.sitemap
#_(:require [clojure.data.xml :as xml])
(:require-macros [com.oly.static-sites.clojure-demos.sitemap]))
#_(defmacro spit-pwa-sitemap
"Generate the sitemap and outut to the provided path"
[path domain urls]
`(io/spit ~path (pwa-sitemap ~domain ~urls)))

View File

@ -0,0 +1,5 @@
(ns com.oly.static-sites.clojure-demos.state
(:require
[reagent.core :as reagent]))
;; Store site state
(defonce site-state (reagent/atom {}))

View File

@ -0,0 +1,51 @@
(ns com.oly.static-sites.clojure-demos.tailwind-theme)
(def tailwind-theme
{:TITLE :header.f2.fw6.f2-ns.lh-title.mt0.mb2
;:DESCRIPTION :p
:HEADER1 :h1.f3.fw6.f3-ns.lh-title.mt0.mb2
:HEADER2 :h2.f3.fw6.f3-ns.lh-title.mt0.mb2
:HEADER3 :h3
:HEADER4 :h4
:HEADER5 :h5
:TODO :span
:VERBATIM :code
:CODE :code
:MAIN :main
:THUMBNAIL nil
:DESCRIPTION :meta ;(fn [v] [:span {:name (str v)} ])
:CATEGORY :meta ;(fn [v] [:span {:name (str v)} ])
:SLUG :meta ;(fn [v] [:span {:name (str v)} ])
:DATE :meta ;(fn [v] [:span {:name (str v)} ])
:FILETAGS :meta ;(fn [v] [:span {:name (str v)} ])
:NAME :meta ;(fn [v] [:span {:name (str v)} ])
:TABLE :table.collapse.ba.br2.b--black-10.pv2.ph3
:TBODY :tbody
:THEAD :thead
:TR :tr.striped--light-gray
:TD :td.pv2.ph3
;;TODO this is a src attrib probably worth handling this differently
;;perhaps allow custom functions instead of a simple replace
:LANGUAGE :class
:SRC :pre.bg-near-black.silver.pa2.hljs.roboto.overflow-auto.klipse
:RESULTS :pre.bg-near-black.silver.pa2.hljs.roboto.overflow-auto
:EXAMPLE :pre.text-slate-700.bg-white.rounded-xl
:COMMENT nil
:CAPTION :span
:I :i
:U :u
:B :b
;;:LINK link-handler
:IMG :img
;; probably remove these 2
:A :a.text-blue-800
:P :p.mb-2
:BR :br
:LINE :p.f5.f4-ns.lh-copy.mt0
:BULLETS-ORDERED :ol
:BULLETS :ul
:BULLET :li})