├── src
├── scss
│ ├── ventas
│ │ ├── pages
│ │ │ └── admin
│ │ │ │ ├── _activity-log.scss
│ │ │ │ ├── taxes
│ │ │ │ └── _edit.scss
│ │ │ │ ├── users
│ │ │ │ └── _edit.scss
│ │ │ │ ├── customization
│ │ │ │ ├── _menus.scss
│ │ │ │ ├── menus
│ │ │ │ │ └── _edit.scss
│ │ │ │ └── _customize.scss
│ │ │ │ ├── _customization.scss
│ │ │ │ ├── _taxes.scss
│ │ │ │ ├── _users.scss
│ │ │ │ ├── _products.scss
│ │ │ │ ├── products
│ │ │ │ └── _edit.scss
│ │ │ │ ├── _shipping-methods.scss
│ │ │ │ ├── _configuration.scss
│ │ │ │ └── _dashboard.scss
│ │ ├── components
│ │ │ ├── _table.scss
│ │ │ ├── _colorpicker.scss
│ │ │ ├── _popup.scss
│ │ │ ├── _draggable-list.scss
│ │ │ ├── _breadcrumbs.scss
│ │ │ ├── _error.scss
│ │ │ ├── _datepicker.scss
│ │ │ ├── _product-filters.scss
│ │ │ ├── _cookies.scss
│ │ │ ├── _search-box.scss
│ │ │ ├── _image.scss
│ │ │ ├── _i18n-input.scss
│ │ │ ├── _term.scss
│ │ │ ├── _notificator.scss
│ │ │ ├── _base.scss
│ │ │ ├── _sidebar.scss
│ │ │ ├── _category-list.scss
│ │ │ ├── _menu.scss
│ │ │ ├── _image-input.scss
│ │ │ ├── _product-list.scss
│ │ │ └── _cart.scss
│ │ └── __variables.scss
│ ├── email.scss
│ └── main.scss
├── clj
│ └── ventas
│ │ ├── email
│ │ ├── templates.clj
│ │ ├── templates
│ │ │ ├── core.clj
│ │ │ ├── password_forgotten.clj
│ │ │ └── user_registered.clj
│ │ └── elements.clj
│ │ ├── storage
│ │ └── protocol.clj
│ │ ├── utils
│ │ ├── files.clj
│ │ ├── jar.clj
│ │ └── slugs.clj
│ │ ├── entities
│ │ ├── event.clj
│ │ ├── site.clj
│ │ ├── core.clj
│ │ ├── product_taxonomy.clj
│ │ ├── state.clj
│ │ ├── product_term.clj
│ │ ├── product_review.clj
│ │ ├── amount.clj
│ │ ├── country.clj
│ │ ├── tax.clj
│ │ ├── brand.clj
│ │ ├── image_size.clj
│ │ └── currency.clj
│ │ ├── auth.clj
│ │ ├── server
│ │ ├── api
│ │ │ └── core.clj
│ │ ├── pagination.clj
│ │ ├── http_ws.clj
│ │ └── admin_spa.clj
│ │ ├── components
│ │ └── crud_form.clj
│ │ ├── database
│ │ ├── generators.clj
│ │ ├── tx_processor.clj
│ │ └── seed.clj
│ │ ├── html.clj
│ │ ├── events.clj
│ │ ├── plugins
│ │ └── menu
│ │ │ └── core.clj
│ │ ├── paths.clj
│ │ ├── search
│ │ ├── entities.clj
│ │ └── schema.clj
│ │ ├── payment_method.clj
│ │ ├── system.clj
│ │ ├── config.clj
│ │ ├── core.clj
│ │ ├── site.clj
│ │ ├── plugin.clj
│ │ └── storage.clj
└── cljs
│ ├── ventas
│ ├── utils
│ │ ├── ui.cljs
│ │ ├── debug.cljs
│ │ ├── goog.cljs
│ │ ├── re_frame.cljs
│ │ ├── formatting.cljs
│ │ ├── logging.cljs
│ │ └── validation.cljs
│ ├── server
│ │ ├── api
│ │ │ ├── user.cljs
│ │ │ ├── description.cljs
│ │ │ └── admin.cljs
│ │ └── api.cljs
│ ├── components
│ │ ├── error.cljs
│ │ ├── payment.cljs
│ │ ├── term.cljs
│ │ ├── category_list.cljs
│ │ ├── colorpicker.cljs
│ │ ├── cookies.cljs
│ │ ├── breadcrumbs.cljs
│ │ ├── sidebar.cljs
│ │ ├── image.cljs
│ │ ├── popup.cljs
│ │ ├── menu.cljs
│ │ ├── zoomable_image.cljs
│ │ ├── popover.cljs
│ │ ├── datepicker.cljs
│ │ ├── crud_form.cljs
│ │ ├── notificator.cljs
│ │ ├── draggable_list.cljs
│ │ ├── amount_input.cljs
│ │ ├── search_box.cljs
│ │ ├── slider.cljs
│ │ └── crud_table.cljs
│ ├── page.cljs
│ ├── plugins
│ │ └── menu
│ │ │ ├── api.cljs
│ │ │ └── core.cljs
│ ├── themes
│ │ └── admin
│ │ │ ├── payment_methods.cljs
│ │ │ ├── customization.cljs
│ │ │ ├── configuration.cljs
│ │ │ ├── common.cljs
│ │ │ ├── taxes.cljs
│ │ │ ├── taxes
│ │ │ └── edit.cljs
│ │ │ ├── customization
│ │ │ └── menus.cljs
│ │ │ ├── plugins.cljs
│ │ │ ├── activity_log.cljs
│ │ │ ├── configuration
│ │ │ ├── general.cljs
│ │ │ └── image_sizes
│ │ │ │ └── edit.cljs
│ │ │ ├── products
│ │ │ └── discounts.cljs
│ │ │ └── products.cljs
│ ├── session.cljs
│ ├── utils.cljs
│ └── local_storage.cljs
│ └── deps.cljs
├── doc
├── devtools.png
├── README.md
├── cljdoc.edn
└── Development_workflow.md
├── resources
├── ventas
│ ├── logo.png
│ ├── config
│ │ └── base-config.edn
│ └── email
│ │ └── elements
│ │ └── email.css
└── logback.xml
├── test
├── cljs
│ └── ventas
│ │ └── core_test.cljs
├── clj
│ └── ventas
│ │ ├── i18n_test.clj
│ │ ├── config_test.clj
│ │ ├── paths_test.clj
│ │ ├── auth_test.clj
│ │ ├── utils
│ │ ├── jar_test.clj
│ │ └── images_test.clj
│ │ ├── events_test.clj
│ │ ├── test_tools.clj
│ │ ├── server
│ │ ├── pagination_test.clj
│ │ ├── ws_test.clj
│ │ └── api
│ │ │ └── description_test.clj
│ │ ├── entities
│ │ ├── file_test.clj
│ │ └── category_test.clj
│ │ ├── email_test.clj
│ │ ├── database
│ │ └── entity_test.clj
│ │ └── utils_test.clj
└── cljc
│ └── ventas
│ └── common
│ └── utils_test.cljc
├── Dockerfile
├── dev
├── cljs
│ └── ventas
│ │ ├── dev.cljs
│ │ ├── themes
│ │ └── dev
│ │ │ ├── core.cljs
│ │ │ └── pages
│ │ │ └── frontend.cljs
│ │ └── devcards
│ │ ├── core.cljs
│ │ ├── menu.cljs
│ │ ├── breadcrumbs.cljs
│ │ ├── category_list.cljs
│ │ └── cart.cljs
└── clj
│ └── repl.clj
├── .travis
└── test.sh
├── .travis.yml
├── .gitignore
├── CONTRIBUTING.md
├── karma.conf.js
├── test-resources
└── logback-test.xml
├── package.json
├── docker-compose.yml
├── shadow-cljs.edn
└── CHANGELOG.md
/src/scss/ventas/pages/admin/_activity-log.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/taxes/_edit.scss:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/users/_edit.scss:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/doc/devtools.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gacelita/ventas/HEAD/doc/devtools.png
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/customization/_menus.scss:
--------------------------------------------------------------------------------
1 |
2 | @import "./menus/edit";
--------------------------------------------------------------------------------
/resources/ventas/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gacelita/ventas/HEAD/resources/ventas/logo.png
--------------------------------------------------------------------------------
/test/cljs/ventas/core_test.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.core-test
2 | (:require
3 | [ventas.common.utils-test]))
4 |
5 |
--------------------------------------------------------------------------------
/src/clj/ventas/email/templates.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.email.templates)
2 |
3 | (defmulti template (fn [template _] template))
4 |
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/_customization.scss:
--------------------------------------------------------------------------------
1 |
2 | @import "./customization/customize";
3 | @import "./customization/menus";
--------------------------------------------------------------------------------
/src/scss/ventas/components/_table.scss:
--------------------------------------------------------------------------------
1 |
2 | .table-component__no-rows {
3 | text-align: center;
4 | padding: 5px 0px;
5 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM anapsix/alpine-java
2 |
3 | ADD target/uberjar/ventas.jar /srv/app.jar
4 | EXPOSE 3450
5 | CMD ["java", "-jar", "/srv/app.jar"]
6 |
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/_taxes.scss:
--------------------------------------------------------------------------------
1 |
2 | .admin-taxes__pagination {
3 | width: 100%;
4 | text-align: center;
5 | }
6 |
7 | @import "./taxes/edit";
--------------------------------------------------------------------------------
/src/cljs/ventas/utils/ui.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.ui)
2 |
3 | (defn with-handler [cb]
4 | (fn [e]
5 | (doto e
6 | .preventDefault
7 | .stopPropagation)
8 | (cb e)))
9 |
--------------------------------------------------------------------------------
/src/scss/ventas/components/_colorpicker.scss:
--------------------------------------------------------------------------------
1 |
2 | .colorpicker {
3 | input {
4 | font-family: sans-serif !important;
5 | padding: 1px 0px !important;
6 | }
7 | }
--------------------------------------------------------------------------------
/src/cljs/ventas/server/api/user.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.server.api.user
2 | (:require-macros
3 | [ventas.server.api.core :refer [define-api-events-for-ns!]]))
4 |
5 | (define-api-events-for-ns!)
--------------------------------------------------------------------------------
/src/cljs/ventas/utils/debug.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.debug
2 | (:require
3 | [cljs.pprint :as pprint]))
4 |
5 | (defn pprint-sub [sub]
6 | [:pre (with-out-str (pprint/pprint @sub))])
7 |
--------------------------------------------------------------------------------
/src/cljs/ventas/server/api/description.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.server.api.description
2 | (:require-macros
3 | [ventas.server.api.core :refer [define-api-events-for-ns!]]))
4 |
5 | (define-api-events-for-ns!)
--------------------------------------------------------------------------------
/src/cljs/ventas/utils/goog.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.goog
2 | (:require
3 | [goog.string :as gstring]
4 | [goog.string.format]))
5 |
6 | (defn format
7 | [fmt & args]
8 | (apply gstring/format fmt args))
9 |
--------------------------------------------------------------------------------
/dev/cljs/ventas/dev.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.dev
2 | "Development-only utilities"
3 | (:require
4 | [cljs.test]
5 | [ventas.core-test]))
6 |
7 | (defn run-tests []
8 | (cljs.test/run-all-tests #"ventas.*?\-test"))
9 |
--------------------------------------------------------------------------------
/src/scss/ventas/components/_popup.scss:
--------------------------------------------------------------------------------
1 |
2 | .ventas {
3 | &.popup__counter {
4 | display: table-cell;
5 | vertical-align: middle;
6 | width: 100%;
7 | text-align: right;
8 | }
9 | }
--------------------------------------------------------------------------------
/src/cljs/ventas/server/api/admin.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.server.api.admin
2 | (:require-macros
3 | [ventas.server.api.admin]
4 | [ventas.server.api.core :refer [define-api-events-for-ns!]]))
5 |
6 | (define-api-events-for-ns!)
--------------------------------------------------------------------------------
/.travis/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | set -e
4 |
5 | npm install
6 | npm install -g karma-cli
7 | npm install -g shadow-cljs
8 |
9 | lein test
10 | shadow-cljs compile :admin-test
11 | karma start --single-run
12 |
--------------------------------------------------------------------------------
/src/clj/ventas/email/templates/core.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.email.templates.core
2 | (:require
3 | [ventas.email.templates.order-status-changed]
4 | [ventas.email.templates.password-forgotten]
5 | [ventas.email.templates.user-registered]))
6 |
--------------------------------------------------------------------------------
/dev/cljs/ventas/themes/dev/core.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.dev.core
2 | (:require
3 | [ventas.core]
4 | [ventas.i18n :as i18n]
5 | [ventas.themes.dev.pages.frontend]))
6 |
7 | (i18n/register-translations!
8 | {:en_US
9 | {}})
10 |
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/_users.scss:
--------------------------------------------------------------------------------
1 |
2 | .admin-users__pagination {
3 | width: 100%;
4 | text-align: center;
5 | }
6 |
7 | .admin-users__table {
8 | td:last-child {
9 | width: 110px;
10 | }
11 | }
12 |
13 | @import "./users/edit";
--------------------------------------------------------------------------------
/test/clj/ventas/i18n_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.i18n-test
2 | (:require
3 | [clojure.test :refer [deftest is testing use-fixtures]]
4 | [ventas.i18n :as sut]))
5 |
6 | (deftest i18n
7 | (is (= (sut/i18n :en_US ::sut/test-value)
8 | "Test value")))
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: clojure
2 | script: "./.travis/test.sh"
3 | cache:
4 | directories:
5 | - $HOME/.m2
6 | dist: trusty
7 | jdk:
8 | - oraclejdk8
9 | after_success:
10 | - CLOVERAGE_VERSION=1.0.10 lein cloverage --codecov
11 | - bash <(curl -s https://codecov.io/bash)
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/_products.scss:
--------------------------------------------------------------------------------
1 |
2 | .admin-products__pagination {
3 | width: 100%;
4 | text-align: center;
5 | }
6 |
7 | .admin-products__table {
8 | td:last-child {
9 | width: 110px;
10 | }
11 | }
12 |
13 | @import "./products/edit";
--------------------------------------------------------------------------------
/src/scss/ventas/components/_draggable-list.scss:
--------------------------------------------------------------------------------
1 | .draggable-list {
2 | display: inline-block;
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | .draggable-list__item {
8 | padding: 5px;
9 |
10 | &:hover, &--active {
11 | background-color: #e3e3e3;
12 | }
13 | }
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/products/_edit.scss:
--------------------------------------------------------------------------------
1 |
2 | .admin-products-edit__variations {
3 | .field {
4 | width: 80px;
5 |
6 | .ui.input input {
7 | padding-left: 0.5rem !important;
8 | padding-right: 0.5rem !important;
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/clj/ventas/storage/protocol.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.storage.protocol)
2 |
3 | (defprotocol StorageBackend
4 | (get-object [this key])
5 | (get-public-url [this key])
6 | (stat-object [this key])
7 | (remove-object [this key])
8 | (list-objects [this] [this prefix])
9 | (put-object [this key file]))
--------------------------------------------------------------------------------
/src/scss/ventas/components/_breadcrumbs.scss:
--------------------------------------------------------------------------------
1 |
2 | .ui.breadcrumb.breadcrumbs {
3 | background-color: white;
4 | border: solid 1px #eee;
5 | padding: 11px 15px;
6 | margin: 10px 0px !important;
7 |
8 | .section.breadcrumbs__breadcrumb {
9 | color: #565656;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/doc/README.md:
--------------------------------------------------------------------------------
1 | ## Documentation
2 |
3 | - [Development workflow](./Development_workflow.md)
4 | - [Endpoints - adding them, using them](./Endpoints.md)
5 | - [Frontend development - components, routes, events, effects...](./Frontend_development.md)
6 | - [Entity types - easily extending the database](./Entity_types.md)
--------------------------------------------------------------------------------
/resources/ventas/config/base-config.edn:
--------------------------------------------------------------------------------
1 | {:database {:url "datomic:free://localhost:4334/ventas"}
2 | :server {:port 3450
3 | :host "localhost"}
4 | :elasticsearch {:index "ventas"
5 | :port 9200
6 | :host "127.0.0.1"}
7 |
8 | ;; used for JWTs
9 | :auth-secret "CHANGEME"}
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/_shipping-methods.scss:
--------------------------------------------------------------------------------
1 | .admin-shipping-methods-edit__pricings.ui.table {
2 | width: auto;
3 |
4 | .field {
5 | width: 80px;
6 |
7 | input {
8 | padding-left: 0.5rem;
9 | padding-right: 0.5rem !important;
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/clj/ventas/utils/files.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.files
2 | (:import
3 | [org.apache.commons.io FilenameUtils]))
4 |
5 | (defn get-tmp-dir []
6 | (System/getProperty "java.io.tmpdir"))
7 |
8 | (defn extension [s]
9 | (FilenameUtils/getExtension s))
10 |
11 | (defn basename [s]
12 | (FilenameUtils/getBaseName s))
13 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/error.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.error
2 | (:require
3 | [ventas.components.base :as base]
4 | [ventas.i18n :refer [i18n]]))
5 |
6 | (defn no-data [& {:keys [message]}]
7 | [:div.error-component.error--no-data
8 | [base/icon {:name "warning sign"}]
9 | [:p (or message (i18n ::no-data))]])
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | profiles.clj
5 | pom.xml
6 | pom.xml.asc
7 | *.jar
8 | *.class
9 | /.lein-*
10 | /.nrepl-port
11 | .hgignore
12 | .hg/
13 | .idea
14 | *.iml
15 | node_modules
16 | .shadow-cljs
17 | .env
18 | /resources/public/files/js
19 | /resources/public/files/css
20 | /storage
21 | /resources/config.edn
--------------------------------------------------------------------------------
/dev/cljs/ventas/devcards/core.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.devcards.core
2 | (:require
3 | [devcards.core :refer-macros [defcard-rg]]
4 | [ventas.devcards.breadcrumbs]
5 | [ventas.devcards.category-list]
6 | [ventas.devcards.menu]
7 | [ventas.devcards.cart]))
8 |
9 | (defn component []
10 | [:div [:h1 "This is your first devcard!"]])
--------------------------------------------------------------------------------
/doc/cljdoc.edn:
--------------------------------------------------------------------------------
1 | {:cljdoc.doc/tree
2 | [["README" {:file "README.md"}]
3 | ["Documentation index" {:file "doc/README.md"}
4 | ["Development workflow" {:file "doc/Development_workflow.md"}]
5 | ["Endpoints" {:file "doc/Endpoints.md"}]
6 | ["Frontend development" {:file "doc/Frontend_development.md"}]
7 | ["Entity types" {:file "doc/Entity_types.md"}]]]}
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/_configuration.scss:
--------------------------------------------------------------------------------
1 |
2 | .admin-configuration__page {
3 | padding: 10px 25px;
4 | }
5 |
6 | .admin-configuration__menu {
7 | .ui.list .item {
8 | padding-bottom: 15px !important;
9 | padding-top: 15px !important;
10 |
11 | .icon {
12 | min-width: 33px;
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/scss/ventas/components/_error.scss:
--------------------------------------------------------------------------------
1 |
2 | .error-component {
3 | text-align: center;
4 | font-size: 1.7rem;
5 | color: #4c4c4c;
6 | padding: 50px 0px;
7 | width: 100%;
8 |
9 | i {
10 | text-align: center;
11 | &:before {
12 | display: block;
13 | }
14 | }
15 | &--no-data {
16 |
17 | }
18 | }
--------------------------------------------------------------------------------
/src/clj/ventas/entities/event.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.event
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [ventas.database.entity :as entity]
5 | [ventas.database.generators :as generators]))
6 |
7 | (spec/def :event/kind ::generators/keyword)
8 |
9 | (spec/def :schema.type/event
10 | (spec/keys :req [:event/kind]))
11 |
12 | (entity/register-type! :event)
13 |
--------------------------------------------------------------------------------
/src/scss/ventas/components/_datepicker.scss:
--------------------------------------------------------------------------------
1 | .date-range-input {
2 | position: relative;
3 |
4 | .ui.input {
5 | min-width: 190px;
6 | }
7 |
8 | .rdr-DateRange {
9 | position: absolute;
10 | border: solid 1px #ccc;
11 | z-index: 1;
12 | top: calc(100% - 5px);
13 | left: 15px;
14 | width: calc(281px * 2);
15 | }
16 | }
--------------------------------------------------------------------------------
/dev/cljs/ventas/devcards/menu.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.devcards.menu
2 | (:require
3 | [devcards.core :refer-macros [defcard-rg]]
4 | [ventas.components.menu :as components.menu]))
5 |
6 | (defn component []
7 | [:div
8 | [components.menu/menu
9 | [{:href "cat" :text "A cat"}
10 | {:href "dog" :text "A doggo"}]]])
11 |
12 | (defcard-rg regular-menu
13 | "Regular menu"
14 | component)
15 |
--------------------------------------------------------------------------------
/src/cljs/ventas/utils/re_frame.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.re-frame)
2 |
3 | (defn pure-reaction [db sub-and-params]
4 | ((re-frame.registrar/get-handler :sub (first sub-and-params))
5 | db
6 | sub-and-params))
7 |
8 | (defn pure-subscribe
9 | "Use subscriptions inside event handlers in a pure way."
10 | [db sub-and-params]
11 | @((re-frame.registrar/get-handler :sub (first sub-and-params))
12 | db
13 | sub-and-params))
--------------------------------------------------------------------------------
/src/cljs/deps.cljs:
--------------------------------------------------------------------------------
1 | {:npm-deps {"react" "^16.8.4"
2 | "js-image-zoom" "^0.5.0"
3 | "moment" "^2.24.0"
4 | "react-color" "^2.17.0"
5 | "react-date-range" "^0.9.4"
6 | "react-dom" "^16.8.4"
7 | "react-sortable-tree" "^2.6.0"
8 | "react-sortable-tree-theme-minimal" "0.0.14"
9 | "semantic-ui-react" "^0.82.5"
10 | "create-react-class" "^15.6.3"}}
--------------------------------------------------------------------------------
/test/clj/ventas/config_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.config-test
2 | (:refer-clojure :exclude [set])
3 | (:require
4 | [clojure.test :refer [deftest is testing]]
5 | [ventas.config :as sut]))
6 |
7 | (deftest config-setting
8 | (sut/set :example :value)
9 | (is (= (sut/get :example) :value)))
10 |
11 | (deftest config-setting-with-vector
12 | (sut/set [:another-example :a] :value)
13 | (is (= (sut/get :another-example :a) :value)))
14 |
--------------------------------------------------------------------------------
/src/scss/ventas/__variables.scss:
--------------------------------------------------------------------------------
1 | $red : #B03060;
2 | $orange : #FE9A76;
3 | $yellow : #FFD700;
4 | $olive : #32CD32;
5 | $green : #016936;
6 | $teal : #008080;
7 | $blue : #0E6EB8;
8 | $violet : #EE82EE;
9 | $purple : #B413EC;
10 | $pink : #FF1493;
11 | $brown : #A52A2A;
12 | $grey : #A0A0A0;
13 | $black : #000000;
14 | $primaryColor : $pink;
15 | $secondaryColor : $grey;
--------------------------------------------------------------------------------
/src/cljs/ventas/page.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.page
2 | "Just the page multimethod"
3 | (:require
4 | [ventas.i18n :refer [i18n]]))
5 |
6 | (defmulti pages identity)
7 |
8 | (defmethod pages :not-found []
9 | [:span
10 | [:h1 (i18n ::not-found)]])
11 |
12 | (defmethod pages :default [route]
13 | [:span
14 | [:h1 (i18n ::not-implemented)]
15 | [:p (i18n ::this-page-has-not-been-implemented route)]])
16 |
17 | (defn main [handler]
18 | [:div#main
19 | [pages handler]])
20 |
--------------------------------------------------------------------------------
/dev/cljs/ventas/devcards/breadcrumbs.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.devcards.breadcrumbs
2 | (:require
3 | [devcards.core :refer-macros [defcard-rg]]
4 | [ventas.components.breadcrumbs :as components.breadcrumbs]))
5 |
6 | (defcard-rg regular-breadcrumb
7 | "Regular breadcrumb"
8 | [components.breadcrumbs/breadcrumb-view :frontend.privacy-policy])
9 |
10 | (defcard-rg parameterized-breadcrumb
11 | "Parameterized breadcrumb"
12 | [components.breadcrumbs/breadcrumb-view :frontend.product {:id 1}])
13 |
--------------------------------------------------------------------------------
/test/clj/ventas/paths_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.paths-test
2 | (:refer-clojure :exclude [resolve])
3 | (:require
4 | [clojure.test :refer [deftest is testing]]
5 | [ventas.paths :as sut]))
6 |
7 | (deftest resolve
8 | (testing "resolve single path"
9 | (is (= (sut/resolve sut/project-resources) "resources")))
10 | (testing "resolve compound path"
11 | (is (= (sut/resolve sut/public) "resources/public"))))
12 |
13 | (deftest path->resource
14 | (is (= (sut/path->resource "resources/test") "test")))
15 |
--------------------------------------------------------------------------------
/src/clj/ventas/entities/site.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.site
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [ventas.database.entity :as entity]))
5 |
6 | (spec/def :schema.type/site
7 | (spec/keys :req [:site/subdomain]))
8 |
9 | (entity/register-type!
10 | :site
11 | {:migrations
12 | [[:base [{:db/ident :site/subdomain
13 | :db/valueType :db.type/string
14 | :db/cardinality :db.cardinality/one
15 | :db/unique :db.unique/identity}]]]
16 |
17 | :autoresolve? true})
18 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ### Contributing
2 |
3 | The best way to contribute is to build a store, and report any problems you encounter or any improvements you'd like.
4 |
5 | You can also contribute with plugins as described in the documentation.
6 |
7 | Finally, you can contact me via Slack/issue/email if you want to take on a task from the backlog.
8 |
9 | PR rules:
10 | - PRs should target `master`
11 | - Keep Travis happy
12 | - It's advisable to add a test for your changes, but don't feel pressured to do it, as most of the project isn't that well tested anyway
13 |
--------------------------------------------------------------------------------
/dev/cljs/ventas/devcards/category_list.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.devcards.category-list
2 | (:require
3 | [devcards.core :refer-macros [defcard-rg]]
4 | [ventas.components.category-list :as components.category-list]))
5 |
6 | (defn- category-list-wrapper []
7 | [components.category-list/category-list
8 | (for [n (range 4)]
9 | {:id (gensym)
10 | :name (random-uuid)
11 | :description "A sample description"})])
12 |
13 | (defcard-rg regular-category-list
14 | "Regular category list"
15 | [category-list-wrapper]
16 | {:inspect-data true})
17 |
--------------------------------------------------------------------------------
/src/clj/ventas/auth.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.auth
2 | (:require
3 | [buddy.sign.jwt :as buddy.jwt]
4 | [ventas.config :as config]
5 | [ventas.database.entity :as entity]
6 | [ventas.utils :as utils]))
7 |
8 | (defn user->token [user]
9 | (buddy.jwt/sign {:user-id (:db/id user)} (config/get :auth-secret)))
10 |
11 | (defn- unsign [token secret]
12 | (utils/swallow
13 | (buddy.jwt/unsign token secret)))
14 |
15 | (defn token->user [token]
16 | (when-let [{:keys [user-id]} (unsign token (config/get :auth-secret))]
17 | (entity/find user-id)))
18 |
--------------------------------------------------------------------------------
/src/cljs/ventas/utils/formatting.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.formatting
2 | (:require
3 | [ventas.common.utils :as common.utils]
4 | [ventas.i18n :refer [i18n]]
5 | [moment]
6 | [ventas.utils.goog :as utils.goog]))
7 |
8 | (defn format-number [n]
9 | (when n
10 | (utils.goog/format (str "%.2f") n)))
11 |
12 | (defn amount->str [{:keys [value currency]}]
13 | (str (format-number (common.utils/bigdec->str value))
14 | " "
15 | (:symbol currency)))
16 |
17 | (defn format-date [date]
18 | (.format (moment. date) "YYYY-MM-DD H:mm:ss"))
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | config.set({
3 | browsers: ['ChromeHeadless'],
4 | // The directory where the output file lives
5 | basePath: 'target',
6 | // The file itself
7 | files: ['karma.cljs'],
8 | frameworks: ['cljs-test'],
9 | plugins: ['karma-cljs-test', 'karma-chrome-launcher'],
10 | colors: true,
11 | logLevel: config.LOG_INFO,
12 | client: {
13 | args: ["shadow.test.karma.init"],
14 | singleRun: true
15 | }
16 | })
17 | };
--------------------------------------------------------------------------------
/test-resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ${PATTERN}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/payment.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.payment
2 | "Entry point to payment methods"
3 | (:refer-clojure :exclude [methods])
4 | (:require [re-frame.core :as rf]))
5 |
6 | (def state-key ::state)
7 |
8 | (defonce ^:private methods (atom {}))
9 |
10 | (defn get-methods []
11 | @methods)
12 |
13 | (defn add-method [kw data]
14 | (swap! methods assoc kw data))
15 |
16 | (rf/reg-sub
17 | ::errors
18 | (fn [db]
19 | (get-in db [state-key :errors])))
20 |
21 | (rf/reg-event-db
22 | ::set-errors
23 | (fn [db [_ errors]]
24 | (assoc-in db [state-key :errors] errors)))
--------------------------------------------------------------------------------
/src/scss/ventas/components/_product-filters.scss:
--------------------------------------------------------------------------------
1 |
2 | .product-filter {
3 | .sidebar-section__content {
4 | .ui.checkbox {
5 | display: block;
6 | margin: 11px 0px;
7 | }
8 | }
9 | }
10 |
11 | .product-filters__term {
12 | &--color {
13 | display: inline-block;
14 | }
15 | }
16 |
17 | .category-term {
18 | cursor: pointer;
19 | padding-left: 12px;
20 |
21 | a {
22 | color: rgba(0,0,0,.87);
23 |
24 | &.category-term--active, &:hover {
25 | font-weight: bold;
26 | }
27 | }
28 |
29 |
30 | }
--------------------------------------------------------------------------------
/test/clj/ventas/auth_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.auth-test
2 | (:require
3 | [clojure.test :refer [deftest is use-fixtures]]
4 | [ventas.auth :as sut]
5 | [ventas.database.entity :as entity]
6 | [ventas.test-tools :as test-tools]))
7 |
8 | (use-fixtures :once
9 | #(test-tools/with-test-context
10 | (%)))
11 |
12 | (def test-user-attrs
13 | {:email "email@email.com"})
14 |
15 | (deftest user->token-and-token->user
16 | (let [user (entity/create :user test-user-attrs)
17 | token (sut/user->token user)]
18 | (is (= (sut/token->user token) user))))
19 |
--------------------------------------------------------------------------------
/src/clj/ventas/utils/jar.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.jar
2 | "See https://stackoverflow.com/questions/22363010/get-list-of-embedded-resources-in-uberjar")
3 |
4 | (def running-jar
5 | "Resolves the path to the current running jar file."
6 | (-> :keyword class (.. getProtectionDomain getCodeSource getLocation getPath)))
7 |
8 | (defn list-resources [& [jar]]
9 | (let [jar (or jar (java.util.jar.JarFile. running-jar))
10 | entries (.entries jar)]
11 | (loop [result []]
12 | (if (.hasMoreElements entries)
13 | (recur (conj result (.. entries nextElement getName)))
14 | result))))
15 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/term.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.term)
2 |
3 | (defmulti term-view (fn [taxonomy-kw _ _] taxonomy-kw))
4 |
5 | (defmethod term-view :color [_ {:keys [color name] :as term} {:keys [on-click active?]}]
6 |
7 | [:div.term.term--color
8 | {:title name
9 | :style {:background-color color}
10 | :class (when active? "term--active")
11 | :on-click on-click}])
12 |
13 | (defmethod term-view :default [_ {:keys [name] :as term} {:keys [active? on-click]}]
14 | [:div.term.term--default
15 | {:class (when active? "term--active")
16 | :on-click on-click}
17 | [:h3 name]])
18 |
--------------------------------------------------------------------------------
/dev/cljs/ventas/themes/dev/pages/frontend.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.dev.pages.frontend
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.components.base :as base]
5 | [ventas.events :as events]
6 | [ventas.events.backend :as backend]
7 | [ventas.routes :as routes]))
8 |
9 | (def state-key ::state)
10 |
11 | (defn page []
12 | [:div.blank-theme__home
13 | [base/container
14 | [:h1 "Test"]]])
15 |
16 | (rf/reg-event-fx
17 | ::init
18 | (fn [_ _]
19 | {}))
20 |
21 | (routes/define-route!
22 | :frontend
23 | {:name "Dev theme - home"
24 | :url ""
25 | :component page
26 | :init-fx [::init]})
27 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/category_list.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.category-list
2 | (:require
3 | [ventas.routes :as routes]))
4 |
5 | (defn category-list [categories]
6 | [:div.category-list
7 | (for [{:keys [id slug name image]} categories]
8 | [:div.category-list__category
9 | {:key id
10 | :on-click #(routes/go-to :frontend.category :id slug)}
11 | (if image
12 | [:img.category-list__image {:src (str "/images/" (:id image) "/resize/category-listing")}]
13 | [:div.category-list__image-placeholder])
14 | [:div.category-list__content
15 | [:h3.category-list__name name]]])])
16 |
--------------------------------------------------------------------------------
/src/scss/ventas/components/_cookies.scss:
--------------------------------------------------------------------------------
1 |
2 | .cookies {
3 | text-align: center;
4 | position: fixed;
5 | bottom: 0;
6 | background-color: #212121;
7 | width: 100%;
8 | color: white;
9 | transition: max-height 300ms;
10 | max-height: 50px;
11 | z-index: 1;
12 | overflow: hidden;
13 | p {
14 | padding: 10px 0px;
15 | margin: 0px;
16 | font-size: 14px;
17 | }
18 | .icon {
19 | position: absolute;
20 | top: 0;
21 | bottom: 0;
22 | margin: auto;
23 | right: 5px;
24 | font-size: 18px;
25 | cursor: pointer;
26 | }
27 | }
--------------------------------------------------------------------------------
/test/clj/ventas/utils/jar_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.jar-test
2 | (:require
3 | [clojure.java.classpath :as classpath]
4 | [clojure.string :as str]
5 | [clojure.test :refer [deftest is testing]]
6 | [ventas.utils.jar :as sut]))
7 |
8 | (def example-jar "org/clojure/clojure/1.9.0/clojure-1.9.0.jar")
9 |
10 | (deftest list-resources
11 | (is (contains? (->> (classpath/classpath-jarfiles)
12 | (filter #(str/includes? (.getName %) example-jar))
13 | (first)
14 | (sut/list-resources)
15 | (set))
16 | "META-INF/maven/org.clojure/clojure/pom.xml")))
17 |
--------------------------------------------------------------------------------
/src/clj/ventas/server/api/core.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.server.api.core
2 | (:require
3 | [ventas.server.api.admin]
4 | [ventas.server.api.description]
5 | [ventas.server.api.user]
6 | [ventas.server.api :as api]))
7 |
8 | (defmacro define-api-events-for-ns! []
9 | (let [ns-name (str (:name (:ns &env)))
10 | endpoints (->> (keys @api/available-requests)
11 | (filter #(= (namespace %) ns-name)))]
12 | `(doseq [~'endpoint [~@endpoints]]
13 | (~'re-frame.core/reg-event-fx
14 | ~'endpoint
15 | (~'fn [~'_ [~'_ ~'options]]
16 | {:ws-request (~'merge {:name ~'endpoint}
17 | ~'options)})))))
--------------------------------------------------------------------------------
/src/cljs/ventas/plugins/menu/api.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.plugins.menu.api
2 | (:require
3 | [re-frame.core :as rf]))
4 |
5 | (rf/reg-event-fx
6 | ::autocompletions.get
7 | (fn [_ [_ options]]
8 | {:ws-request (merge {:name :ventas.plugins.menu.core/autocompletions.get}
9 | options)}))
10 |
11 | (rf/reg-event-fx
12 | ::routes.get-name
13 | (fn [_ [_ options]]
14 | {:ws-request (merge {:name :ventas.plugins.menu.core/routes.get-name}
15 | options)}))
16 |
17 | (rf/reg-event-fx
18 | ::menus.get
19 | (fn [_ [_ options]]
20 | {:ws-request (merge {:name :ventas.plugins.menu.core/menu.get}
21 | options)}))
--------------------------------------------------------------------------------
/src/scss/ventas/components/_search-box.scss:
--------------------------------------------------------------------------------
1 | .search-result {
2 | padding: 10px;
3 | cursor: pointer;
4 |
5 | &:hover {
6 | background-color: #eaeaea;
7 | }
8 |
9 | img {
10 | display: inline-block;
11 | vertical-align: middle;
12 | margin-right: 10px;
13 | border-radius: 5px;
14 | }
15 |
16 | }
17 |
18 | .search-result__right {
19 | display: inline-block;
20 | vertical-align: middle;
21 |
22 | p {
23 | margin: 0;
24 | }
25 | }
26 |
27 | .search-result__name {
28 | font-weight: bold;
29 | }
30 |
31 | .search-box.ui.selection.dropdown .menu>.item {
32 | padding: 0 !important;
33 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "create-react-class": "^15.6.3",
4 | "js-image-zoom": "^0.5.0",
5 | "moment": "^2.24.0",
6 | "react": "^16.8.4",
7 | "react-color": "^2.17.0",
8 | "react-date-range": "^0.9.4",
9 | "react-dom": "^16.8.4",
10 | "react-sortable-tree": "^2.6.0",
11 | "react-sortable-tree-theme-minimal": "0.0.14",
12 | "semantic-ui-react": "^0.82.5",
13 | "xregexp": "^4.2.0"
14 | },
15 | "devDependencies": {
16 | "babel-core": "^6.26.3",
17 | "babel-preset-env": "^1.7.0",
18 | "karma": "^2.0.2",
19 | "karma-chrome-launcher": "^2.2.0",
20 | "karma-cljs-test": "^0.1.0",
21 | "showdown": "^1.9.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/scss/ventas/components/_image.scss:
--------------------------------------------------------------------------------
1 |
2 | .image-component {
3 | max-width: 100%;
4 | max-height: 100%;
5 | overflow: hidden;
6 |
7 | img {
8 | min-height: 100%;
9 | }
10 | }
11 |
12 | .image-component__inner {
13 | width: 200%;
14 | height: 100%;
15 | left: -50%;
16 | position: relative;
17 | text-align: center;
18 | }
19 |
20 | .image-component__dimmer {
21 | width: 100%;
22 | height: 100%;
23 | position: relative;
24 |
25 | .ui.dimmer {
26 | z-index: unset;
27 | }
28 | }
29 |
30 | .image-component__error {
31 | text-align: center;
32 | display: flex;
33 | align-items: center;
34 |
35 | > div {
36 | width: 100%;
37 | }
38 | }
--------------------------------------------------------------------------------
/src/clj/ventas/components/crud_form.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.components.crud-form
2 | (:require
3 | [slingshot.slingshot :refer [throw+]]))
4 |
5 | (defmacro field
6 | "Automates form's `label`
7 | **ClojureScript only**"
8 | [state-path {:keys [key] :as args}]
9 | (if-not (:ns &env)
10 | `(throw+ {:type ::unsupported-environment
11 | :message "This macro is cljs-only"})
12 | (let [caller-ns (str (:name (:ns &env)))
13 | key (if (sequential? key)
14 | (first key)
15 | key)
16 | kw (keyword caller-ns (name key))]
17 | `[~'ventas.components.form/field
18 | (~'merge ~args
19 | {:db-path ~state-path
20 | :label (~'ventas.i18n/i18n ~kw)})])))
--------------------------------------------------------------------------------
/src/clj/ventas/email/templates/password_forgotten.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.email.templates.password-forgotten
2 | (:require
3 | [ventas.auth :as auth]
4 | [ventas.email.elements :as elements]
5 | [ventas.email.templates :as templates]
6 | [ventas.i18n :refer [i18n]]
7 | [ventas.entities.user :as entities.user]
8 | [ventas.entities.i18n :as entities.i18n]))
9 |
10 | (defmethod templates/template :password-forgotten [_ {:keys [user]}]
11 | {:body
12 | (let [culture-kw (entities.i18n/culture->kw
13 | (entities.user/get-culture user))]
14 | (elements/skeleton
15 | user
16 | [:p
17 | [:a {:href (elements/get-url (str "/profile/account?token=" (auth/user->token user)))}
18 | (i18n culture-kw ::reset-your-password)]]))})
19 |
--------------------------------------------------------------------------------
/src/scss/ventas/components/_i18n-input.scss:
--------------------------------------------------------------------------------
1 | .i18n-input {
2 |
3 | .ui.input input, .ui.input textarea {
4 | border-top-left-radius: 0px !important;
5 | border-bottom-left-radius: 0px !important;
6 | }
7 |
8 | &--focused {
9 | margin-bottom: 15px;
10 | .i18n-input__cultures {
11 | display: block;
12 | }
13 | }
14 | }
15 |
16 | .i18n-input__cultures {
17 | display: none;
18 | }
19 |
20 | .i18n-input__culture {
21 | padding: 0px 8px;
22 | background: #f6f6f6;
23 | border: 1px solid rgba(34,36,38,.15);
24 | border-right: 0px;
25 | display: flex;
26 | align-items: center;
27 | border-radius: .28571429rem;
28 | border-top-right-radius: 0px;
29 | border-bottom-right-radius: 0px;
30 | }
--------------------------------------------------------------------------------
/test/clj/ventas/events_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.events-test
2 | (:require
3 | [clojure.core.async :as core.async :refer [! go]]
4 | [clojure.test :refer [deftest is testing use-fixtures]]
5 | [ventas.events :as sut]))
6 |
7 | (deftest register-pub-sub
8 | (let [ch (go
9 | (let [data {:some :data}]
10 | (sut/register-event! :test)
11 | (is (= (keys (sut/event :test)) [:chan :mult]))
12 | (let [ch1 (sut/sub :test)
13 | ch2 (sut/sub :test)]
14 | (>! (sut/pub :test) data)
15 | (is (= data
16 | (> @(rf/subscribe [::admin.skeleton/menu-items])
11 | (filter #(= (:route %) :admin.payment-methods))
12 | first
13 | :children))
14 |
15 | (defn- page []
16 | [admin.skeleton/skeleton
17 | [:div.admin__default-content.admin-payment-methods__page
18 | [admin.configuration/menu (get-items)]]])
19 |
20 | (routes/define-route!
21 | :admin.payment-methods
22 | {:name ::page
23 | :url "payment-methods"
24 | :component page})
25 |
--------------------------------------------------------------------------------
/test/cljc/ventas/common/utils_test.cljc:
--------------------------------------------------------------------------------
1 | (ns ventas.common.utils-test
2 | #? (:cljs (:require-macros [cljs.test :refer [is deftest testing]]))
3 | (:require [ventas.common.utils :as sut]
4 | #?(:clj [clojure.test :refer :all]
5 | :cljs [cljs.test])))
6 |
7 | (deftest read-keyword
8 | (is (= :keyword (sut/read-keyword ":keyword")))
9 | (is (= :test/keyword (sut/read-keyword ":test/keyword"))))
10 |
11 | (deftest map-values
12 | (is (= {:a 2 :b 3} (sut/map-vals inc {:a 1 :b 2}))))
13 |
14 | (deftest deep-merge
15 | (testing "deep-merge merges maps recursively"
16 | (is (= (sut/deep-merge
17 | {:a {:b {:c 1 :d {:x 1 :y 2}} :e 3} :f 4}
18 | {:a {:b {:c 2 :d {:z 9} :z 3} :e 100}})
19 | {:a {:b {:z 3, :c 2, :d {:z 9, :x 1, :y 2}}, :e 100}, :f 4}))))
20 |
--------------------------------------------------------------------------------
/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ${PATTERN}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/clj/ventas/database/generators.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.database.generators
2 | "User-friendly generators"
3 | (:require
4 | [clojure.spec.alpha :as spec]
5 | [clojure.string :as str]
6 | [clojure.test.check.generators :as gen]
7 | [ventas.utils :as utils]))
8 |
9 | (defn string-generator []
10 | (gen/fmap str/join
11 | (gen/vector gen/char-alphanumeric 2 10)))
12 |
13 | (defn keyword-generator []
14 | (gen/fmap (comp keyword str/lower-case)
15 | (string-generator)))
16 |
17 | (spec/def ::string
18 | (spec/with-gen string? string-generator))
19 |
20 | (spec/def ::keyword
21 | (spec/with-gen keyword? keyword-generator))
22 |
23 | (spec/def ::bigdec
24 | (spec/with-gen
25 | utils/bigdec?
26 | (fn []
27 | (gen/fmap bigdec
28 | (gen/double* {:NaN? false :min 0 :max 999})))))
29 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/colorpicker.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.colorpicker
2 | (:require
3 | [react-color]
4 | [re-frame.core :as rf]))
5 |
6 | (def chrome-picker (js/React.createFactory (.-ChromePicker react-color)))
7 |
8 | (defn colorpicker [{:keys [on-change value]}]
9 | [:div.colorpicker
10 | (chrome-picker
11 | (->> {:disableAlpha false
12 | :color value
13 | :onChangeComplete (fn [color _]
14 | (rf/dispatch (conj on-change (:hex (js->clj color :keywordize-keys true)))))}
15 | (remove (fn [[k v]] (nil? v)))
16 | (into {})
17 | clj->js))])
18 |
19 | (defn colorpicker-input [{:keys [on-change value]}]
20 | [:div.colorpicker-input
21 | [:input {:value (or value "")}]
22 | [colorpicker {:on-change on-change
23 | :value value}]])
--------------------------------------------------------------------------------
/src/clj/ventas/email/templates/user_registered.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.email.templates.user-registered
2 | (:require
3 | [ventas.email.elements :as elements]
4 | [ventas.email.templates :as templates]
5 | [ventas.entities.configuration :as entities.configuration]
6 | [ventas.i18n :refer [i18n]]
7 | [ventas.entities.user :as entities.user]
8 | [ventas.entities.i18n :as entities.i18n]))
9 |
10 | (defmethod templates/template :user-registered [_ {:keys [user]}]
11 | {:body
12 | (let [culture-kw (entities.i18n/culture->kw
13 | (entities.user/get-culture user))]
14 | (elements/skeleton
15 | user
16 | [:p (i18n culture-kw ::welcome (entities.configuration/get :customization/name))]
17 | [:p (i18n culture-kw ::add-an-address)]
18 | [:p
19 | [:a {:href (elements/get-url "/profile")}
20 | (i18n culture-kw ::go-to-profile)]]))})
21 |
--------------------------------------------------------------------------------
/src/clj/ventas/html.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.html
2 | (:require [clojure.string :as str]
3 | [hiccup.core :as hiccup]))
4 |
5 | (defn enqueue-resource [type request name file]
6 | (assoc-in request [::resources type name] file))
7 |
8 | (def enqueue-css (partial enqueue-resource :css))
9 | (def enqueue-js (partial enqueue-resource :js))
10 |
11 | (defn- ->script [src]
12 | [:script {:src src}])
13 |
14 | (defmulti resource->html (fn [type _] type))
15 |
16 | (defmethod resource->html :js [_ resource]
17 | (hiccup/html [:script {:src resource}]))
18 |
19 | (defmethod resource->html :css [_ resource]
20 | (hiccup/html [:link {:href resource :rel "stylesheet" :type "text/css"}]))
21 |
22 | (defn html-resources [request]
23 | (->> (::resources request)
24 | (mapcat (fn [[type resource-map]]
25 | (map (partial resource->html type) (vals resource-map))))
26 | (str/join)))
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 |
4 | datomic:
5 | image: akiel/datomic-free:0.9.5561
6 | ports:
7 | - 4334:4334
8 | - 4335:4335
9 | - 4336:4336
10 | volumes:
11 | - datomic:/opt/datomic-pro-0.9.5561/data
12 | environment:
13 | - XMX=-Xmx512M
14 | - XMS=-Xms512M
15 | - ALT_HOST=127.0.0.1
16 |
17 | elasticsearch:
18 | image: docker.elastic.co/elasticsearch/elasticsearch:6.1.1
19 | environment:
20 | - cluster.name=docker-cluster
21 | - bootstrap.memory_lock=true
22 | - xpack.security.enabled=false
23 | - cluster.routing.allocation.disk.threshold_enabled=false
24 | ulimits:
25 | memlock:
26 | soft: -1
27 | hard: -1
28 | volumes:
29 | - elasticsearch:/usr/share/elasticsearch/data
30 | ports:
31 | - 9200:9200
32 |
33 | volumes:
34 | elasticsearch:
35 | driver: local
36 | datomic:
37 | driver: local
38 |
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/customization.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.customization
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.i18n :refer [i18n]]
5 | [ventas.themes.admin.skeleton :as admin.skeleton]
6 | [ventas.themes.admin.customization.customize]
7 | [ventas.themes.admin.customization.menus]
8 | [ventas.themes.admin.configuration :as admin.configuration]
9 | [ventas.routes :as routes])
10 | (:require-macros
11 | [ventas.utils :refer [ns-kw]]))
12 |
13 | (defn get-items []
14 | (->> @(rf/subscribe [::admin.skeleton/menu-items])
15 | (filter #(= (:route %) :admin.customization))
16 | first
17 | :children))
18 |
19 | (defn- page []
20 | [admin.skeleton/skeleton
21 | [:div.admin__default-content.admin-customization__page
22 | [admin.configuration/menu (get-items)]]])
23 |
24 | (routes/define-route!
25 | :admin.customization
26 | {:name ::page
27 | :url "customization"
28 | :component page})
--------------------------------------------------------------------------------
/src/cljs/ventas/components/cookies.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.cookies
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.components.base :as base]))
5 |
6 | (def state-key ::state)
7 |
8 | (rf/reg-event-fx
9 | ::get-state-from-local-storage
10 | [(rf/inject-cofx :local-storage)]
11 | (fn [{:keys [db local-storage]} [_]]
12 | {:db (assoc db state-key (get local-storage state-key))}))
13 |
14 | (rf/reg-sub
15 | ::open?
16 | (fn [db]
17 | (not (get db state-key))))
18 |
19 | (rf/reg-event-fx
20 | ::close
21 | [(rf/inject-cofx :local-storage)]
22 | (fn [{:keys [db local-storage]}]
23 | {:db (assoc db state-key true)
24 | :local-storage (assoc local-storage state-key true)}))
25 |
26 | (defn cookies [text]
27 | "Cookie warning"
28 | (let [open? @(rf/subscribe [::open?])]
29 | [:div.cookies {:style (when-not open? {:max-height "0px"})}
30 | [:p text]
31 | [base/icon {:name "remove"
32 | :on-click #(rf/dispatch [::close])}]]))
33 |
--------------------------------------------------------------------------------
/src/cljs/ventas/server/api.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.server.api
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.common.utils :as common.utils])
5 | (:require-macros
6 | [ventas.server.api.core :refer [define-api-events-for-ns!]]))
7 |
8 | (define-api-events-for-ns!)
9 |
10 | (rf/reg-event-fx
11 | ::categories.list
12 | (fn [_ [_ options]]
13 | {:ws-request (common.utils/deep-merge
14 | {:name ::categories.list
15 | :params {:pagination {:page 0 :items-per-page 5}}}
16 | options)}))
17 |
18 | (rf/reg-event-fx
19 | ::entities.find
20 | (fn [_ [_ id options]]
21 | {:ws-request (merge {:name ::entities.find
22 | :params {:id id}}
23 | options)}))
24 |
25 | (rf/reg-event-fx
26 | ::products.list
27 | (fn [_ [_ options]]
28 | {:ws-request (common.utils/deep-merge
29 | {:name ::products.list
30 | :params {:pagination {:page 0 :items-per-page 5}}}
31 | options)}))
32 |
--------------------------------------------------------------------------------
/src/cljs/ventas/utils/logging.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.logging
2 | "Simple wrapper for js/console")
3 |
4 | (def log-level :debug)
5 |
6 | (def ^:private log-levels [:trace :debug :info :warn :error])
7 |
8 | (defn- log [level data]
9 | {:pre [(contains? (set log-levels) level)]}
10 | (let [index (.indexOf log-levels level)
11 | min-index (.indexOf log-levels log-level)]
12 | (when (<= min-index index)
13 | (case level
14 | :trace (apply js/console.log "TRACE [] - " data)
15 | :debug (apply js/console.log "DEBUG [] - " data)
16 | :info (apply js/console.info "INFO [] - " data)
17 | :warn (apply js/console.warn "WARN [] - " data)
18 | :error (apply js/console.error "ERROR [] - " data)))))
19 |
20 | (defn trace [& data]
21 | (log :trace data))
22 |
23 | (defn debug [& data]
24 | (log :debug data))
25 |
26 | (defn info [& data]
27 | (log :info data))
28 |
29 | (defn warn [& data]
30 | (log :warn data))
31 |
32 | (defn error [& data]
33 | (log :error data))
34 |
--------------------------------------------------------------------------------
/src/scss/ventas/components/_term.scss:
--------------------------------------------------------------------------------
1 | .term.term--color {
2 | width: 18px;
3 | height: 18px;
4 | border: solid 2px white;
5 | box-shadow: 0px 0px 0px 2px #d4d4d4;
6 | display: inline-block;
7 | background: white;
8 | margin: 4px;
9 | line-height: 35px;
10 | text-align: center;
11 | box-sizing: content-box;
12 | cursor: pointer;
13 | border-radius: 50%;
14 |
15 | &.term--active, &:hover {
16 | box-shadow: 0px 0px 0px 2px #9e9e9e;
17 | }
18 | }
19 |
20 | .term.term--default {
21 | display: inline-block;
22 | background: white;
23 | margin: 5px;
24 | width: 35px;
25 | height: 35px;
26 | line-height: 35px;
27 | text-align: center;
28 | border: solid 2px transparent;
29 | box-sizing: content-box;
30 | cursor: pointer;
31 | border-radius: 50%;
32 |
33 | h3 {
34 | line-height: inherit;
35 | font-weight: normal;
36 | }
37 |
38 | &:hover, &.term--active {
39 | border: solid 2px #c3c3c3;
40 | }
41 | }
--------------------------------------------------------------------------------
/src/scss/ventas/components/_notificator.scss:
--------------------------------------------------------------------------------
1 |
2 | .notificator {
3 | position: fixed;
4 | top: 30px;
5 | right: 20px;
6 | overflow: visible;
7 | z-index: 99999;
8 | width: 270px;
9 | height: 0px;
10 | }
11 | .notificator__item {
12 | display: inline-block;
13 | height: auto;
14 | background-color: #fbfbfb;
15 | padding: 15px;
16 | width: 100%;
17 | box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.5);
18 | position: relative;
19 | margin-top: 15px;
20 | .close {
21 | position: absolute;
22 | top: 5px;
23 | right: 5px;
24 | }
25 | &.danger {
26 | color: #a94442;
27 | background-color: #f2dede;
28 | border-color: #ebccd1;
29 | }
30 | &.warning {
31 | color: #8a6d3b;
32 | background-color: #fcf8e3;
33 | border-color: #faebcc;
34 | }
35 | &.info {
36 | color: #31708f;
37 | background-color: #d9edf7;
38 | border-color: #bce8f1;
39 | }
40 | &.success {
41 | color: #3c763d;
42 | background-color: #dff0d8;
43 | border-color: #d6e9c6;
44 | }
45 | }
--------------------------------------------------------------------------------
/src/scss/ventas/components/_base.scss:
--------------------------------------------------------------------------------
1 |
2 | .ui.button {
3 | position: relative;
4 | }
5 |
6 | .ui.selection.dropdown {
7 | border-color: rgba(34,36,38,.35);
8 | box-shadow: none;
9 | }
10 |
11 | body .ui.header {
12 | font-weight: normal;
13 | }
14 |
15 | .ui.grid.smaller-padding {
16 | & > .column:not(.row), & > .row > .column {
17 | padding: 0px 0.8rem 1.6rem 0.8rem;
18 | }
19 | }
20 |
21 | .segment-title {
22 | position: absolute;
23 | bottom: 100%;
24 | color: white;
25 | padding: 4px 39px 4px 15px;
26 | border-top-left-radius: .28571429rem;
27 | border-top-right-radius: .28571429rem;
28 | left: -1px;
29 | font-size: 1.1rem;
30 | }
31 |
32 | .ui.segment.segment--with-title {
33 | border-top-left-radius: 0px;
34 | margin-top: calc(1rem + 27px);
35 |
36 | &:first-child {
37 | margin-top: 27px;
38 | }
39 | }
40 |
41 | .ui.dropdown {
42 | &.dropdown--align-right {
43 | .menu {
44 | left: auto;
45 | right: 0px;
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/scss/ventas/components/_sidebar.scss:
--------------------------------------------------------------------------------
1 |
2 | .sidebar {
3 | width: 300px;
4 | padding-right: 8px;
5 | }
6 |
7 | .sidebar-section {
8 | background-color: #ececec;
9 | padding: 13px;
10 | margin-bottom: 20px;
11 |
12 | &.sidebar-section--closed {
13 | .sidebar-section__content {
14 | height: 0px;
15 | }
16 | }
17 | }
18 |
19 | .sidebar-section__header {
20 | border-bottom: solid 1px #cacaca;
21 | display: flex;
22 | padding-bottom: 9px;
23 | cursor: pointer;
24 | padding-top: 10px;
25 | margin-top: -10px;
26 | color: rgba(0,0,0,.87);
27 |
28 | h2 {
29 | font-size: 1.2rem;
30 | flex: 1;
31 | margin-bottom: 0px;
32 | }
33 | }
34 |
35 | .sidebar-section__content {
36 | overflow: hidden;
37 | margin-top: 10px;
38 | }
39 |
40 | .sidebar-link {
41 | display: block;
42 | padding: 5px 0px;
43 | padding-left: 20px;
44 | color: #333333;
45 | cursor: pointer;
46 |
47 | &:hover {
48 | background-color: #d8d8d8;
49 | color: inherit;
50 | }
51 | }
--------------------------------------------------------------------------------
/src/cljs/ventas/components/breadcrumbs.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.breadcrumbs
2 | (:require
3 | [ventas-bidi-syntax.core :as bidi-syntax]
4 | [ventas.components.base :as base]
5 | [ventas.routes :as routes]
6 | [ventas.utils :as util]))
7 |
8 | (defn- breadcrumb-data [handler params]
9 | (map (fn [route] {:url (apply routes/path-for route (first (seq params)))
10 | :name (routes/route-name route params)
11 | :route route})
12 | (conj (bidi-syntax/route-parents handler)
13 | handler)))
14 |
15 | (defn breadcrumb-view [handler params]
16 | [base/breadcrumb {:class "breadcrumbs"}
17 | (doall
18 | (util/interpose-fn
19 | (fn [] [base/breadcrumb-divider {:key (gensym)}])
20 | (for [{:keys [name url route]} (breadcrumb-data handler params)]
21 | [base/breadcrumb-section
22 | {:key route
23 | :class "breadcrumbs__breadcrumb"
24 | :href url}
25 | name])))])
26 |
27 | (defn breadcrumbs []
28 | (let [[handler params] (routes/current)]
29 | [breadcrumb-view handler params]))
30 |
--------------------------------------------------------------------------------
/src/scss/ventas/components/_category-list.scss:
--------------------------------------------------------------------------------
1 |
2 | .category-list {
3 | display: flex;
4 | flex-direction: row;
5 | flex-wrap: wrap;
6 | margin: 30px 0px 40px;
7 | }
8 |
9 | .category-list__category {
10 | width: 25%;
11 | overflow: hidden;
12 | padding: 0px 15px;
13 | cursor: pointer;
14 | transition: all 200ms;
15 |
16 | img {
17 | width: 100%;
18 | }
19 |
20 | &:hover {
21 | transform: scale(1.03, 1.03);
22 | }
23 | }
24 |
25 | .category-list__name {
26 | color: black;
27 | text-overflow: ellipsis;
28 | font-size: 1.3rem;
29 | color: black;
30 | text-transform: uppercase;
31 | margin: 10px 0px !important;
32 | font-weight: normal;
33 | }
34 |
35 | .category-list__content {
36 | text-overflow: ellipsis;
37 | text-align: center;
38 |
39 | p {
40 | font-family: 'Raleway',sans-serif;
41 | font-size: 14px;
42 | color: #676767;
43 | }
44 | }
45 |
46 | .category-list__image-placeholder {
47 | width: 100%;
48 | padding-bottom: 131%;
49 | background-color: #cccccc;
50 | }
--------------------------------------------------------------------------------
/src/cljs/ventas/components/sidebar.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.sidebar
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.components.base :as base]))
5 |
6 | (def state-key ::state)
7 |
8 | (rf/reg-event-db
9 | ::toggle-filter
10 | (fn [db [_ id]]
11 | (update-in db [state-key id :closed] not)))
12 |
13 | (defn link [attrs label]
14 | [:a.sidebar-link attrs label])
15 |
16 | (defn sidebar-section [{:keys [name id]} & args]
17 | (let [id (if-not id (str (gensym)) id)]
18 | (fn [{:keys [name id]} & args]
19 | (let [{:keys [closed]} @(rf/subscribe [:db [state-key id]])]
20 | [:div.sidebar-section {:class (str "sidebar-section--" (if closed "closed" "open"))}
21 | [:div.sidebar-section__header
22 | {:on-click #(rf/dispatch [::toggle-filter id])}
23 | [:h2 name]
24 | [base/icon {:name (str "chevron " (if closed "down" "up"))}]]
25 | [:div.sidebar-section__content
26 | args]]))))
27 |
28 | (defn sidebar [& children]
29 | [:div.sidebar
30 | (map-indexed
31 | (fn [idx child]
32 | (with-meta child {:key idx}))
33 | children)])
34 |
--------------------------------------------------------------------------------
/src/cljs/ventas/plugins/menu/core.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.plugins.menu.core
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.components.menu :as menu]
5 | [ventas.plugins.menu.api :as api]
6 | [ventas.routes :as routes]))
7 |
8 | (def state-key ::state)
9 |
10 | (rf/reg-event-fx
11 | ::init
12 | (fn [_ [_ state-id menu-id]]
13 | {:dispatch [::api/menus.get {:params {:id menu-id}
14 | :success [:db [state-key state-id]]}]}))
15 |
16 | (defn- ->menu-item [{:keys [name link children]}]
17 | {:text name
18 | :href (if (vector? link)
19 | (apply routes/path-for link)
20 | link)
21 | :target (when-not (vector? link)
22 | "_blank")
23 | :children (map ->menu-item children)})
24 |
25 | (rf/reg-sub
26 | ::items
27 | (fn [db [_ state-id]]
28 | (let [items (get-in db [state-key state-id :items])]
29 | (->> items
30 | (map ->menu-item)))))
31 |
32 | (defn menu [state-id]
33 | [menu/menu
34 | {:items @(rf/subscribe [::items state-id])
35 | ;; @TODO Figure this out using the current route
36 | :current-fn (constantly nil)}])
--------------------------------------------------------------------------------
/src/clj/ventas/entities/product_taxonomy.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.product-taxonomy
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [ventas.database.entity :as entity]
5 | [ventas.database.generators :as generators]
6 | [ventas.entities.i18n :as entities.i18n]))
7 |
8 | (spec/def :product.taxonomy/name ::entities.i18n/ref)
9 |
10 | (spec/def :product.taxonomy/keyword ::generators/keyword)
11 |
12 | (spec/def :schema.type/product.taxonomy
13 | (spec/keys :req [:product.taxonomy/name]
14 | :opt [:product.taxonomy/keyword]))
15 |
16 | (entity/register-type!
17 | :product.taxonomy
18 | {:migrations
19 | [[:base [{:db/ident :product.taxonomy/name
20 | :db/valueType :db.type/ref
21 | :db/cardinality :db.cardinality/one
22 | :db/isComponent true
23 | :ventas/refEntityType :i18n}
24 |
25 | {:db/ident :product.taxonomy/keyword
26 | :db/valueType :db.type/keyword
27 | :db/unique :db.unique/identity
28 | :db/cardinality :db.cardinality/one}]]]
29 |
30 | :dependencies
31 | #{:i18n}
32 |
33 | :autoresolve? true})
34 |
--------------------------------------------------------------------------------
/src/clj/ventas/events.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.events
2 | (:require
3 | [clojure.core.async :as core.async :refer [! chan go-loop sliding-buffer]]))
4 |
5 | (defonce events (atom {}))
6 |
7 | (defn register-event! [evt-name]
8 | (let [ch (chan (sliding-buffer 10))
9 | evt-data {:chan ch :mult (core.async/mult ch)}]
10 | (swap! events assoc evt-name evt-data)
11 | evt-data))
12 |
13 | (defn event [evt-name]
14 | (if-let [evt-data (get @events evt-name)]
15 | evt-data
16 | (register-event! evt-name)))
17 |
18 | (defn pub [evt-name]
19 | (let [data (event evt-name)]
20 | (get data :chan)))
21 |
22 | (defn sub [evt-name]
23 | (let [data (event evt-name)
24 | ch (chan)]
25 | (core.async/tap (get data :mult) ch)
26 | ch))
27 |
28 | ;; cljs
29 | (defmacro ns-subscribe [path]
30 | (let [caller-ns (str (:name (:ns &env)))]
31 | `(deref (re-frame.core/subscribe [:db (into [(keyword ~caller-ns "state")] ~path)]))))
32 |
33 | (defmacro ns-dispatch [path value]
34 | (let [caller-ns (str (:name (:ns &env)))]
35 | `(re-frame.core/dispatch [:db (into [(keyword ~caller-ns "state")] ~path) ~value])))
--------------------------------------------------------------------------------
/src/clj/ventas/database/tx_processor.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.database.tx-processor
2 | (:require
3 | [mount.core :refer [defstate]]
4 | [clojure.tools.logging :as log]
5 | [ventas.utils :as utils]
6 | [ventas.database :as db]))
7 |
8 | (def ^:private callbacks (atom {}))
9 |
10 | (defn add-callback! [id cb]
11 | (swap! callbacks assoc id cb))
12 |
13 | (defn remove-callback! [id]
14 | (swap! callbacks dissoc id))
15 |
16 | (defn start-tx-report-queue-loop! []
17 | (future
18 | (loop []
19 | (when-not (Thread/interrupted)
20 | (utils/interruptible-try
21 | (when-let [report (.take (db/tx-report-queue))]
22 | (doseq [[id callback] @callbacks]
23 | (try
24 | (log/debug "Processing" id "callback")
25 | (callback report)
26 | (catch Throwable e
27 | (log/error (str "Error executing tx-processor callback with ID " id) e))))))
28 | (recur)))))
29 |
30 | (defstate tx-processor
31 | :start
32 | (do
33 | (log/info "Starting tx-processor")
34 | (start-tx-report-queue-loop!))
35 | :stop
36 | (do
37 | (log/info "Stopping tx-processor")
38 | (future-cancel tx-processor)))
--------------------------------------------------------------------------------
/src/clj/ventas/plugins/menu/core.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.plugins.menu.core
2 | (:require
3 | [ventas.server.api :as api]
4 | [ventas.i18n :refer [i18n]]
5 | [ventas.common.utils :refer [find-first]]))
6 |
7 | (defonce ^:private config (atom {}))
8 |
9 | (defn setup! [m]
10 | (reset! config m))
11 |
12 | (defn- call-fn [kw & args]
13 | (let [f (get @config kw)]
14 | (when-not f
15 | (throw (Exception. "Menu has not been set up")))
16 | (apply f args)))
17 |
18 | (defn- find-routes [input culture]
19 | (call-fn :find-routes input culture))
20 |
21 | (defn- route->name [route culture]
22 | (call-fn :route->name route culture))
23 |
24 | (api/register-endpoint!
25 | ::autocompletions.get
26 | (fn [{{:keys [query]} :params} {:keys [session]}]
27 | (let [culture (api/get-culture session)]
28 | (find-routes query culture))))
29 |
30 | (api/register-endpoint!
31 | ::routes.get-name
32 | (fn [{{:keys [route]} :params} {:keys [session]}]
33 | (let [culture (api/get-culture session)]
34 | (route->name route culture))))
35 |
36 | (api/register-endpoint!
37 | ::menu.get
38 | (fn [{{:keys [id]} :params} {:keys [session]}]
39 | (api/find-serialize-with-session session id)))
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/configuration.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.configuration
2 | "Mobile-only page listing configuration sections"
3 | (:require
4 | [ventas.i18n :refer [i18n]]
5 | [ventas.themes.admin.skeleton :as admin.skeleton]
6 | [ventas.routes :as routes]
7 | [re-frame.core :as rf]
8 | [ventas.components.base :as base]))
9 |
10 | (defn menu-item [{:keys [route label icon]}]
11 | [base/list-item {:href (routes/path-for route)}
12 | (when icon
13 | [base/list-icon {:name icon :size "large" :vertical-align "middle"}])
14 | [base/list-content
15 | [base/list-header
16 | (i18n label)]]])
17 |
18 | (defn get-items []
19 | (->> @(rf/subscribe [::admin.skeleton/menu-items])
20 | (filter :configuration?)))
21 |
22 | (defn menu [items]
23 | [:div.admin-configuration__menu
24 | [base/list {:divided true :relaxed true}
25 | (for [item items]
26 | [menu-item item])]])
27 |
28 | (defn page []
29 | [admin.skeleton/skeleton
30 | [:div.admin__default-content.admin-configuration__page
31 | [menu (get-items)]]])
32 |
33 | (routes/define-route!
34 | :admin.configuration
35 | {:name ::page
36 | :url "configuration"
37 | :component page})
38 |
--------------------------------------------------------------------------------
/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | {:lein {:profile "+dev"}
2 | :nrepl {:port 4002}
3 | :builds {:admin {:target :browser
4 | :asset-path "files/js/admin"
5 | :output-to "resources/public/files/js/admin/main.js"
6 | :output-dir "resources/public/files/js/admin"
7 | :modules {:main {:entries [ventas.themes.admin.core]}}
8 | :devtools {:after-load ventas.core/on-reload
9 | :watch-dir "resources/public"}
10 | :dev {:preloads [devtools.preload]
11 | :compiler-options {:devcards true}}
12 | :release {:pretty-print false}}
13 | :admin-test {:target :karma
14 | :output-to "target/karma.cljs"}
15 | :devcards {:target :browser
16 | :asset-path "files/js/devcards"
17 | :output-to "resources/public/files/js/devcards/main.js"
18 | :output-dir "resources/public/files/js/devcards"
19 | :dev {:preloads [devtools.preload]
20 | :compiler-options {:devcards true}}
21 | :modules {:main {:entries [ventas.devcards.core]}}}}}
--------------------------------------------------------------------------------
/resources/ventas/email/elements/email.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0; }
4 |
5 | img {
6 | border: 0 none;
7 | height: auto;
8 | line-height: 100%;
9 | outline: none;
10 | text-decoration: none; }
11 |
12 | a img {
13 | border: 0 none; }
14 |
15 | .imageFix {
16 | display: block; }
17 |
18 | table, td {
19 | border-collapse: collapse; }
20 |
21 | .order__total,
22 | .order__image {
23 | padding: 10px 0px; }
24 |
25 | .table-header {
26 | padding: 10px 0px;
27 | font-size: 15px; }
28 |
29 | .logo {
30 | padding-bottom: 40px; }
31 |
32 | #bodyTable {
33 | height: 100% !important;
34 | margin: 0;
35 | padding: 0;
36 | width: 100% !important;
37 | font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
38 | font-size: 14px;
39 | line-height: 20px;
40 | color: #212121;
41 | font-smoothing: antialiased; }
42 |
43 | h1, h2, h3, h4, h5 {
44 | line-height: 23px;
45 | margin-bottom: 14px;
46 | font-weight: bold;
47 | padding: 0; }
48 |
49 | p {
50 | margin-bottom: 14px;
51 | line-height: 20px; }
52 |
53 | .email-templates {
54 | margin: 20px; }
55 |
56 | .email-template-wrapper {
57 | width: 560px;
58 | border: solid 1px #e0e0e0;
59 | margin-bottom: 30px; }
60 |
--------------------------------------------------------------------------------
/src/clj/ventas/entities/state.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.state
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [ventas.database.entity :as entity]
5 | [ventas.entities.i18n :as entities.i18n]))
6 |
7 | (spec/def :state/name ::entities.i18n/ref)
8 |
9 | (spec/def :schema.type/state
10 | (spec/keys :req [:state/name]))
11 |
12 | (entity/register-type!
13 | :state
14 | {:migrations
15 | [[:base [{:db/ident :state/keyword
16 | :db/valueType :db.type/keyword
17 | :db/unique :db.unique/identity
18 | :db/cardinality :db.cardinality/one}
19 |
20 | {:db/ident :state/country
21 | :db/valueType :db.type/ref
22 | :db/cardinality :db.cardinality/one
23 | :ventas/refEntityType :country}
24 |
25 | {:db/ident :state/parent
26 | :db/valueType :db.type/ref
27 | :db/cardinality :db.cardinality/one
28 | :ventas/refEntityType :state}
29 |
30 | {:db/ident :state/name
31 | :db/valueType :db.type/ref
32 | :db/cardinality :db.cardinality/one
33 | :db/isComponent true
34 | :ventas/refEntityType :i18n}]]]
35 |
36 | :autoresolve? true
37 | :dependencies #{:i18n :country}})
38 |
--------------------------------------------------------------------------------
/src/clj/ventas/paths.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.paths
2 | (:refer-clojure :exclude [resolve])
3 | (:require
4 | [clojure.java.io :as io]
5 | [clojure.string :as str]))
6 |
7 | (def project-resources
8 | "A path for project-wide resources, like the configuration"
9 | ::project-resources)
10 |
11 | (def public
12 | "Files accessible via HTTP"
13 | ::public)
14 |
15 | (def public-files
16 | "Non-HTML public files. This is a necessary because of routing limitations."
17 | ::public-files)
18 |
19 | (def seeds
20 | "Where the files for seeding live"
21 | ::seeds)
22 |
23 | (def ^:private paths
24 | {project-resources "resources"
25 | public [project-resources "/public"]
26 | public-files [public "/files"]
27 | seeds [project-resources "/seeds"]})
28 |
29 | (defn- resolve-path [v]
30 | (if (string? v)
31 | v
32 | (let [path (get paths v)]
33 | (if (string? path)
34 | path
35 | (apply str (map resolve-path path))))))
36 |
37 | (defn resolve
38 | "Resolves a path, makes sure it exists and returns it"
39 | [kw]
40 | (let [path (resolve-path kw)]
41 | (io/make-parents (str path "/."))
42 | path))
43 |
44 | (defn path->resource [path]
45 | (str/replace path (str (resolve-path project-resources) "/") ""))
46 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/image.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.image
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.components.base :as base]))
5 |
6 | (def state-key ::state)
7 |
8 | (defn get-url [id & [size]]
9 | (if size
10 | (str "images/" id "/resize/" (name size))
11 | (str "images/" id)))
12 |
13 | (defn image [id size]
14 | {:pre [(keyword? size)]}
15 | (let [status @(rf/subscribe [:db [state-key [id size]]])]
16 | (when-let [{:keys [width height]} @(rf/subscribe [:db [:image-sizes size]])]
17 | [:div.image-component {:style {:width (dec width)
18 | :height height}}
19 | (when-not status
20 | [:div.image-component__dimmer
21 | [base/loading]])
22 | (when (= :status/error status)
23 | [:div.image-component__error
24 | [:div
25 | [base/icon {:name "image"}]
26 | [:p "Error loading this image"]]])
27 | [:div.image-component__inner
28 | [:img {:style (when-not (= status :status/loaded) {:display "none"})
29 | :on-load #(rf/dispatch [:db [state-key [id size]] :status/loaded])
30 | :on-error #(rf/dispatch [:db [state-key [id size]] :status/error])
31 | :src (get-url id size)}]]])))
32 |
--------------------------------------------------------------------------------
/src/cljs/ventas/utils/validation.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.validation
2 | (:require
3 | [clojure.string :as str]))
4 |
5 | (defn length-validator [{:keys [min max] :or {min -1}} value]
6 | (if-not (seqable? value)
7 | true
8 | (let [length (count value)
9 | max (or max (inc (count value)))]
10 | (< min length max))))
11 |
12 | (defn email-validator [_ value]
13 | (or (empty? value)
14 | (str/includes? value "@")))
15 |
16 | (defn required-validator [_ value]
17 | (if (or (string? value) (coll? value))
18 | (not (empty? value))
19 | value))
20 |
21 | (defn validate [field-validators field value]
22 | (let [validators (get field-validators field)
23 | results (map (fn [[identifier validation-fn params]]
24 | {:identifier identifier
25 | :params params
26 | :valid? (validation-fn params value)})
27 | validators)]
28 | {:valid? (every? identity (map :valid? results))
29 | :infractions (->> results
30 | (map (fn [{:keys [identifier valid? params]}]
31 | (when-not valid?
32 | [identifier params])))
33 | (remove (fn [[k v]] (nil? v)))
34 | (into {}))}))
35 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/popup.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.popup
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.components.base :as base]))
5 |
6 | (def data-key ::popup)
7 |
8 | (rf/reg-event-db
9 | ::close
10 | (fn [db [_]]
11 | (-> db (update data-key drop-last))))
12 |
13 | (rf/reg-event-db
14 | ::show
15 | (fn [db [_ title message]]
16 | (let [data {:open true :message message :title title}]
17 | (if (seq (get db data-key))
18 | (update db data-key conj data)
19 | (assoc db data-key [data])))))
20 |
21 | (defn popup
22 | "A popup, useful for displaying messages to the user"
23 | []
24 | [:div.popup
25 | (let [items @(rf/subscribe [:db [data-key]])]
26 | (when-let [data (last items)]
27 | [base/modal {:basic true :open (:open data) :size "small"}
28 | [base/header
29 | [base/icon {:name "remove"}]
30 | [:div.content {:title data}]
31 | [:div.popup__counter
32 | (str (count items) "/" (count items))]]
33 | [base/modal-content
34 | [:p (:message data)]]
35 | [base/modal-actions
36 | [base/button {:color "green"
37 | :inverted true
38 | :on-click #(rf/dispatch [::close])}
39 | [base/icon {:name "checkmark"}]
40 | "OK"]]]))])
41 |
--------------------------------------------------------------------------------
/test/clj/ventas/test_tools.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.test-tools
2 | (:require
3 | [datomic.api :as d]
4 | [ventas.database :as db]
5 | [clojure.java.io :as io]
6 | [ventas.database.seed :as seed]
7 | [ventas.entities.core]
8 | [ventas.storage :as storage]
9 | [ventas.config :as config]
10 | [mount.core :as mount]))
11 |
12 | (defn create-test-uri [& [id]]
13 | (str "datomic:mem://" (or id (gensym "test"))))
14 |
15 | (defn test-conn [& [id]]
16 | (let [uri (create-test-uri id)]
17 | (d/create-database uri)
18 | (let [c (d/connect uri)]
19 | (with-redefs [db/conn c]
20 | (seed/seed))
21 | c)))
22 |
23 | (defn test-storage-backend []
24 | (let [tmp-dir (System/getProperty "java.io.tmpdir")
25 | base-path (str tmp-dir "/" "ventas-test-storage")]
26 | (.mkdir (io/file base-path))
27 | (storage/->LocalStorageBackend base-path)))
28 |
29 | (defmacro with-test-context [& body]
30 | `(with-redefs [db/conn (test-conn)
31 | storage/storage-backend (test-storage-backend)]
32 | (mount/start #'config/config)
33 | ~@body))
34 |
35 | (defn with-test-image [f]
36 | (with-open [is (io/input-stream (io/resource "ventas/logo.png"))]
37 | (let [temp-file (java.io.File/createTempFile "ventas-logo" ".png")]
38 | (io/copy is temp-file)
39 | (f temp-file))))
--------------------------------------------------------------------------------
/src/scss/email.scss:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | img {
7 | border: 0 none;
8 | height: auto;
9 | line-height: 100%;
10 | outline: none;
11 | text-decoration: none;
12 | }
13 |
14 | a img {
15 | border: 0 none;
16 | }
17 |
18 | .imageFix {
19 | display: block;
20 | }
21 |
22 | table, td {
23 | border-collapse: collapse;
24 | }
25 |
26 | .order__total,
27 | .order__image {
28 | padding: 10px 0px;
29 | }
30 |
31 | .table-header {
32 | padding: 10px 0px;
33 | font-size: 15px;
34 | }
35 |
36 | .logo {
37 | padding-bottom: 40px;
38 | }
39 |
40 | #bodyTable {
41 | height:100% !important;
42 | margin:0;
43 | padding:0;
44 | width:100% !important;
45 | font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
46 | font-size: 14px;
47 | line-height: 20px;
48 | color: #212121;
49 | font-smoothing: antialiased;
50 | }
51 |
52 | h1, h2, h3, h4, h5 {
53 | line-height: 23px;
54 | margin-bottom: 14px;
55 | font-weight: bold;
56 | padding: 0;
57 | }
58 |
59 | p {
60 | margin-bottom: 14px;
61 | line-height: 20px;
62 | }
63 |
64 | .email-templates {
65 | margin: 20px;
66 | }
67 |
68 | .email-template-wrapper {
69 | width: 560px;
70 | border: solid 1px #e0e0e0;
71 | margin-bottom: 30px;
72 | }
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/scss/ventas/components/_menu.scss:
--------------------------------------------------------------------------------
1 |
2 | .menu {
3 | background-color: rgba(246,246,246,0.8);
4 | position: relative;
5 | border: 1px solid rgb(222, 222, 222);
6 | border-width: 1px 0px;
7 | }
8 |
9 | .menu__items {
10 | padding: 0px;
11 | margin: 0px;
12 | text-align: center;
13 | }
14 |
15 | .menu__item {
16 | display: inline-block;
17 | padding: 10px 0px;
18 |
19 | > a {
20 | font-family: 'Raleway',sans-serif;
21 | font-weight: bold;
22 | font-size: 15px;
23 | color: #212121;
24 | text-transform: uppercase;
25 | display: block;
26 | border-radius: 9px;
27 | padding: 8px 10px;
28 | text-decoration: none !important;
29 | }
30 |
31 | &.menu__item--active, &:hover {
32 | > a {
33 | color: #888;
34 | }
35 | }
36 |
37 | &:hover {
38 | .menu__overlay {
39 | display: block;
40 | }
41 | }
42 | }
43 |
44 | .menu__overlay {
45 | width: 100%;
46 | position: absolute;
47 | left: 0;
48 | top: 100%;
49 | z-index: 1;
50 | background: white;
51 | border-bottom: 1px solid rgb(222, 222, 222);
52 | display: none;
53 | }
54 |
55 | .menu__child {
56 | padding: 8px;
57 | display: inline-block;
58 | color: inherit;
59 | font-size: 1.05rem;
60 | }
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/common.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.common
2 | (:require
3 | [clojure.set :as set]
4 | [re-frame.core :as rf]
5 | [ventas.components.form :as form]
6 | [ventas.server.api.admin :as api.admin]))
7 |
8 | (def state-key ::state)
9 |
10 | (rf/reg-event-fx
11 | ::search
12 | (fn [_ [_ key attrs search]]
13 | {:dispatch [::api.admin/admin.search
14 | {:params {:search search
15 | :attrs attrs}
16 | :success [:db [state-key :search-results key]]}]}))
17 |
18 | (defn entity->option [entity]
19 | (-> entity
20 | (select-keys #{:id :name})
21 | (set/rename-keys {:id :value
22 | :name :text})))
23 |
24 | (defn entity-search-field [{:keys [db-path label key attrs selected-option]}]
25 | [form/field {:db-path db-path
26 | :label label
27 | :key key
28 | :type :entity
29 | :on-search-change #(rf/dispatch [::search key attrs (-> % .-target .-value)])
30 | :options (->> @(rf/subscribe [:db [state-key :search-results key]])
31 | (map entity->option)
32 | (into (if selected-option
33 | [(entity->option selected-option)]
34 | [])))}])
35 |
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/taxes.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.taxes
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.components.table :as table]
5 | [ventas.components.crud-table :as crud-table]
6 | [ventas.i18n :refer [i18n]]
7 | [ventas.themes.admin.skeleton :as admin.skeleton]
8 | [ventas.themes.admin.taxes.edit]
9 | [ventas.routes :as routes]))
10 |
11 | (def state-path [::state :table])
12 |
13 | (defn- page []
14 | [admin.skeleton/skeleton
15 | [:div.admin__default-content.admin-taxes__page
16 | [:div.admin-taxes__table
17 | [table/table state-path]]]])
18 |
19 | (rf/reg-event-fx
20 | ::init
21 | (fn [_ _]
22 | {:dispatch [::crud-table/init state-path
23 | {:columns [{:id :name
24 | :label (i18n ::name)
25 | :component (partial table/link-column :admin.taxes.edit :id :name)}
26 | {:id :amount
27 | :label (i18n ::amount)
28 | :component (partial table/amount-column :amount)}
29 | (crud-table/action-column state-path)]
30 | :edit-route :admin.taxes.edit
31 | :entity-type :tax}]}))
32 |
33 | (routes/define-route!
34 | :admin.taxes
35 | {:name ::page
36 | :url "taxes"
37 | :component page
38 | :init-fx [::init]})
39 |
--------------------------------------------------------------------------------
/test/clj/ventas/server/pagination_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.server.pagination-test
2 | (:require
3 | [clojure.test :refer [deftest is testing]]
4 | [ventas.server.pagination :as sut]))
5 |
6 | (deftest wrap-paginate
7 | (let [request {:params {:pagination {:items-per-page 2
8 | :page 2}}}]
9 | (is (= {:request request
10 | :response {:items [4]
11 | :total 5}}
12 | (sut/wrap-paginate {:request request
13 | :response (range 5)})))))
14 |
15 | (deftest wrap-sort
16 | (let [request {:params {:sorting {:field :name
17 | :direction :asc}}}
18 | items [{:name :x :other :field}
19 | {:name :yy :other :field}
20 | {:name :c :other :field}]]
21 | (testing "sorts"
22 | (is (= {:request request
23 | :response (sort-by :name items)}
24 | (sut/wrap-sort {:request request
25 | :response items}))))
26 | (testing "sorts with DESC direction"
27 | (let [request (assoc-in request [:params :sorting :direction] :desc)]
28 | (is (= {:request request
29 | :response (reverse (sort-by :name items))}
30 | (sut/wrap-sort {:request request
31 | :response items})))))))
32 |
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/customization/menus/_edit.scss:
--------------------------------------------------------------------------------
1 |
2 | .admin-menus-edit__placeholder {
3 | background: red !important;
4 | bottom: -3px;
5 | left: 0;
6 | height: 2px;
7 | width: 100%;
8 | position: relative;
9 |
10 | span {
11 | background: red !important;
12 | border-radius: 50%;
13 | position: absolute;
14 | width: 13px;
15 | height: 13px;
16 | bottom: -9px;
17 | left: -11px;
18 | }
19 | }
20 |
21 | .admin-menus-edit__sortable-tree {
22 | height: 100%;
23 | margin: 10px 0px;
24 | }
25 |
26 | .admin-menus-edit__items {
27 |
28 | .draggable-list {
29 | list-style-type: none;
30 | }
31 |
32 | .draggable-list__item {
33 | position: relative;
34 | margin-bottom: 8px;
35 | }
36 | }
37 |
38 | .admin-menus-edit__add-item {
39 | margin-left: 5px !important;
40 | margin-top: 5px !important;
41 | }
42 |
43 | .admin-menus-edit__menu-item {
44 | min-width: 400px;
45 |
46 | &--parent {
47 | &::after, &::before {
48 | left: 20px !important;
49 | width: calc(100% - 20px);
50 | }
51 | }
52 |
53 | .ui.segment {
54 | display: flex;
55 | align-items: center;
56 | }
57 |
58 | .icon {
59 | margin-right: 10px;
60 | }
61 |
62 | span {
63 | flex-grow: 1;
64 | }
65 | }
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/taxes/edit.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.taxes.edit
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.components.base :as base]
5 | [ventas.events :as events]
6 | [ventas.i18n :refer [i18n]]
7 | [ventas.themes.admin.skeleton :as admin.skeleton]
8 | [ventas.routes :as routes]
9 | [ventas.components.crud-form :as crud-form :include-macros true]))
10 |
11 | (def state-path [::state])
12 |
13 | (rf/reg-event-fx
14 | ::init
15 | (fn [_ _]
16 | {:dispatch-n [[::events/enums.get :tax.kind]
17 | [::crud-form/init state-path :tax]]}))
18 |
19 | (defn content []
20 | [base/segment {:color "orange"
21 | :title "Tax"}
22 | (crud-form/field
23 | state-path
24 | {:key :tax/name
25 | :type :i18n})
26 |
27 | (crud-form/field
28 | state-path
29 | {:key :tax/amount
30 | :type :amount})
31 |
32 | (crud-form/field
33 | state-path
34 | {:key [:tax/kind :db/id]
35 | :type :combobox
36 | :options @(rf/subscribe [:db [:enums :tax.kind]])})])
37 |
38 | (defn page []
39 | [admin.skeleton/skeleton
40 | [:div.admin__default-content.admin-taxes-edit__page
41 | [crud-form/component state-path :admin.taxes
42 | [content]]]])
43 |
44 | (routes/define-route!
45 | :admin.taxes.edit
46 | {:name ::page
47 | :url [:id "/edit"]
48 | :component page
49 | :init-fx [::init]})
50 |
--------------------------------------------------------------------------------
/src/scss/ventas/components/_image-input.scss:
--------------------------------------------------------------------------------
1 |
2 | .image-input {
3 | display: inline-block;
4 | position: relative;
5 | height: 150px;
6 |
7 | img.ui.image {
8 | margin: 0;
9 | }
10 |
11 | .ui.icon {
12 | position: absolute;
13 | top: 5px;
14 | right: 5px;
15 | }
16 | }
17 |
18 | .ui.small.image.image-input__placeholder {
19 | background-color: #d0d0d0;
20 | font-size: 2.5rem;
21 | height: 150px;
22 |
23 | .icon {
24 | position: absolute;
25 | top: 0;
26 | bottom: 0;
27 | left: 0;
28 | right: 0;
29 | margin: auto;
30 | line-height: normal;
31 | }
32 |
33 | input {
34 | display: none;
35 | }
36 | }
37 |
38 |
39 | .image-input__list-element {
40 | display: inline-block;
41 | position: relative;
42 |
43 | img.ui.image {
44 | margin: 0;
45 | }
46 |
47 | .ui.icon {
48 | position: absolute;
49 | top: 5px;
50 | right: 5px;
51 | }
52 | }
53 |
54 | .image-input__list {
55 | .ui.small.image {
56 | height: 150px;
57 | }
58 |
59 | .draggable-list {
60 | margin-right: 5px;
61 | margin-bottom: 5px;
62 | }
63 |
64 | .draggable-list__item {
65 | display: inline-block;
66 | }
67 |
68 | .image-input__placeholder {
69 | margin: 0px;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/clj/ventas/utils/slugs.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.slugs
2 | (:require
3 | [cuerdas.core :as cuerdas]
4 | [ventas.common.utils :as common.utils]
5 | [ventas.database.entity :as entity]
6 | [ventas.entities.i18n :as entities.i18n]
7 | [ventas.database :as db]))
8 |
9 | (defn slug [s]
10 | ;; cuerdas is just an implementation detail, that's why we're wrapping it
11 | (cuerdas/slug s))
12 |
13 | (defn- slugify-i18n* [i18n]
14 | {:pre [(entity/entity? i18n)]}
15 | (if (:db/id i18n)
16 | (->> (entity/serialize i18n)
17 | (common.utils/map-vals slug)
18 | (entities.i18n/->entity))
19 | (update i18n
20 | :i18n/translations
21 | (fn [translations]
22 | (map #(update % :i18n.translation/value slug)
23 | translations)))))
24 |
25 | (defn slugify-i18n [i18n]
26 | (slugify-i18n*
27 | (if (number? i18n)
28 | (entity/find i18n)
29 | i18n)))
30 |
31 | (defn add-slug-to-entity [entity source-attr]
32 | (let [source (get entity source-attr)]
33 | (if (and (not (:ventas/slug entity)) source)
34 | (assoc entity :ventas/slug (slugify-i18n source))
35 | entity)))
36 |
37 | (defn resolve-by-slug [slug]
38 | (db/nice-query-attr
39 | {:find '[?id]
40 | :in {'?slug slug}
41 | :where '[[?translation :i18n.translation/value ?slug]
42 | [?i18n :i18n/translations ?translation]
43 | [?id :ventas/slug ?i18n]]}))
44 |
--------------------------------------------------------------------------------
/src/clj/ventas/search/entities.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.search.entities
2 | "Generic fulltext entity search"
3 | (:require
4 | [ventas.database.entity :as entity]
5 | [ventas.database :as db]
6 | [ventas.utils :as utils]
7 | [ventas.search :as search]
8 | [ventas.search.indexing :refer [subfield-property]]))
9 |
10 | (defn- prepare-search-attrs
11 | "Applies the culture to the idents that refer to i18n entities.
12 | :product/name -> :product/name__en_US
13 | :product/reference -> :product/reference"
14 | [attrs culture-kw]
15 | (for [attr attrs]
16 | (let [{:ventas/keys [refEntityType]} (db/etouch attr)]
17 | (if-not (= refEntityType :i18n)
18 | attr
19 | (subfield-property attr culture-kw)))))
20 |
21 | (defn search
22 | "Fulltext search for `search` in the given `attrs`"
23 | [text attrs culture-id]
24 | {:pre [(utils/check ::entity/ref culture-id)]}
25 | (let [culture (entity/find culture-id)
26 | shoulds (for [attr (prepare-search-attrs attrs (:i18n.culture/keyword culture))]
27 | {:match {attr text}})]
28 | (->> (get-in (search/search {:query {:bool {:should shoulds}}
29 | :_source false})
30 | [:body :hits :hits])
31 | (map :_id)
32 | (map (fn [v] (Long/parseLong v)))
33 | (map #(entity/find-serialize % {:culture (:db/id culture)
34 | :keep-type? true})))))
35 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/menu.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.menu
2 | (:require
3 | [ventas.components.base :as base]
4 | [cljs.spec.alpha :as spec]
5 | [ventas.utils :as utils]))
6 |
7 | (spec/def ::href string?)
8 | (spec/def ::text string?)
9 | (spec/def ::target string?)
10 | (spec/def ::children
11 | (spec/coll-of ::item))
12 |
13 | (spec/def ::item
14 | (spec/keys :opt-un [::href
15 | ::text
16 | ::children
17 | ::target]))
18 |
19 | (spec/def ::items
20 | (spec/coll-of ::item))
21 |
22 | (defn menu-children [children]
23 | [:div.menu__children
24 | (for [{:keys [id text href target] :as child} children]
25 | ^{:key (or id (hash child))}
26 | [:a.menu__child {:href href
27 | :target target} text])])
28 |
29 | (defn menu-item [{:keys [href target text children] :as a} current?]
30 | [:li.menu__item
31 | {:class (when current? "menu__item--active")}
32 | [:a {:href href
33 | :target target}
34 | text]
35 | (when children
36 | [:div.menu__overlay
37 | [base/container
38 | [menu-children children]]])])
39 |
40 | (defn menu [{:keys [current-fn items]}]
41 | {:pre [(utils/check ::items items)]}
42 | [:div.menu
43 | [base/container
44 | [:ul.menu__items
45 | (doall
46 | (for [item items]
47 | ^{:key (or (:id item) (hash item))}
48 | [menu-item item (current-fn item)]))]]])
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/_dashboard.scss:
--------------------------------------------------------------------------------
1 | @import "../../_variables";
2 |
3 | .admin-dashboard__users {
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | .admin-dashboard__user {
9 | padding: 8px 0px;
10 | display: flex;
11 | border-bottom: solid 1px #ddd;
12 | flex-direction: row;
13 |
14 | > a {
15 | display: flex;
16 | width: 100%;
17 |
18 | p:first-child {
19 | width: 100%;
20 | }
21 | }
22 |
23 | p {
24 | white-space: nowrap;
25 | margin-bottom: 0;
26 | }
27 | }
28 |
29 | .admin-dashboard__traffic-statistics {
30 | .ui.segment {
31 | display: flex;
32 | flex-wrap: wrap;
33 | }
34 |
35 | .ui.header {
36 | width: 100%;
37 | flex: 1;
38 | }
39 | }
40 |
41 | .pending-orders__order {
42 | color: #4183c4;
43 | cursor: pointer;
44 |
45 | &--acknowledged {
46 | background-color: lighten($yellow, 45);
47 | }
48 | &--ready {
49 | background-color: lighten($green, 73);
50 | }
51 | &--unpaid {
52 | background-color: lighten($red, 48);
53 | }
54 | &--paid {
55 | background-color: lighten($orange, 23);
56 | }
57 | }
58 |
59 | .ui.button.pending-orders__button {
60 | background: white;
61 | color: black;
62 | border: solid 1px #b9b9b9;
63 | }
64 |
65 | .pending-orders__address {
66 | display: inline-block;
67 | vertical-align: top;
68 | }
--------------------------------------------------------------------------------
/src/clj/ventas/server/pagination.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.server.pagination
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [ventas.utils :as utils]))
5 |
6 | (defn- limit [coll offset quantity]
7 | (let [offset (or offset 0)]
8 | (take quantity (drop offset coll))))
9 |
10 | (spec/def ::page number?)
11 | (spec/def ::items-per-page number?)
12 | (spec/def ::pagination
13 | (spec/keys :req-un [::page ::items-per-page]))
14 |
15 | (defn paginate [coll {:keys [items-per-page page] :as pagination}]
16 | {:pre [(or (nil? pagination) (utils/check ::pagination pagination))]}
17 | (if pagination
18 | {:total (count coll)
19 | :items (limit coll
20 | (* items-per-page page)
21 | items-per-page)}
22 | coll))
23 |
24 | (defn wrap-paginate [previous]
25 | (let [pagination (get-in previous [:request :params :pagination])]
26 | (-> previous
27 | (update :response #(paginate % pagination)))))
28 |
29 | (defn- sort* [coll {:keys [field direction]}]
30 | (let [sorter (if (sequential? field)
31 | #(get-in % field)
32 | field)
33 | sorted (sort-by sorter coll)]
34 | (if (= direction :asc)
35 | sorted
36 | (reverse sorted))))
37 |
38 | (defn wrap-sort [previous]
39 | (let [config (get-in previous [:request :params :sorting])]
40 | (if (or (not (sequential? (:response previous)))
41 | (not config))
42 | previous
43 | (-> previous
44 | (update :response #(sort* % config))))))
45 |
--------------------------------------------------------------------------------
/src/clj/ventas/payment_method.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.payment-method
2 | "The API for registering payment methods.
3 | Payment methods are responsible for:
4 | - Transforming an :order entity into the shape they need
5 | - Making the HTTP requests they need and handling them correctly
6 | - Updating the :order with the resulting :payment-reference and :payment-amount
7 | - Changing the status of the :order from :draft to :paid or :unpaid
8 | If an error occurs, it should be registered in the event log, and the order should be
9 | left unmodified.
10 |
11 | To allow the payment methods to do their work, they can do anything they need, but most likely
12 | they'll want to:
13 | - Register Ring handlers: use ventas.plugin's :http-handler
14 | - Make HTTP requests: `clj-http` is included as a dependency"
15 | (:require
16 | [slingshot.slingshot :refer [throw+]]
17 | [ventas.database.entity :as entity]
18 | [ventas.plugin :as plugin]))
19 |
20 | (defn register! [kw attrs]
21 | (plugin/register! kw (merge attrs
22 | {:type :payment-method})))
23 |
24 | (defn all []
25 | (plugin/by-type :payment-method))
26 |
27 | (defn pay!
28 | "Launches a payment method for an order"
29 | [order params]
30 | {:pre [(:order/payment-method order) (entity/entity? order)]}
31 | (let [method (plugin/find (:order/payment-method order))]
32 | (when-not method
33 | (throw+ {:type ::payment-method-not-found
34 | :method method}))
35 | ((:pay-fn method) order params)))
36 |
--------------------------------------------------------------------------------
/src/cljs/ventas/session.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.session
2 | (:require
3 | [cljs.core.async :as core.async :refer [>! chan go]]
4 | [day8.re-frame.forward-events-fx]
5 | [re-frame.core :as rf]))
6 |
7 | (def ready
8 | "A value will be put here when the session is ready"
9 | (chan))
10 |
11 | (rf/reg-event-fx
12 | ::listen-to-events
13 | (fn [_ _]
14 | {:forward-events {:register ::listener
15 | :events #{:ventas.events/session.start :ventas.events/session.error}
16 | :dispatch-to [::session-done]}}))
17 |
18 | (rf/dispatch [::listen-to-events])
19 |
20 | (rf/reg-event-fx
21 | ::session-done
22 | (fn [_ [_ [event-kw message]]]
23 | (core.async/put! ready {:success (= event-kw :ventas.events/session.start)
24 | :message message})
25 | {:forward-events {:unregister ::listener}}))
26 |
27 | (defn- get-identity [db]
28 | (get-in db [:session :identity]))
29 |
30 | (rf/reg-sub ::identity get-identity)
31 |
32 | (rf/reg-sub
33 | ::culture
34 | :<- [::identity]
35 | (fn [identity]
36 | (:culture identity)))
37 |
38 | (rf/reg-sub
39 | ::culture-keyword
40 | :<- [::culture]
41 | (fn [culture]
42 | (:keyword culture)))
43 |
44 | (rf/reg-sub
45 | ::culture-id
46 | :<- [::culture]
47 | (fn [culture]
48 | (:id culture)))
49 |
50 | (defn- identity-valid? [{:keys [id status]}]
51 | (and id
52 | (not= :user.status/unregistered status)))
53 |
54 | (rf/reg-sub
55 | ::identity.valid?
56 | (fn [_]
57 | (rf/subscribe [::identity]))
58 | identity-valid?)
59 |
60 |
--------------------------------------------------------------------------------
/test/clj/ventas/entities/file_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.file-test
2 | (:require
3 | [clojure.test :refer [deftest is testing use-fixtures]]
4 | [ventas.database.entity :as entity]
5 | [ventas.entities.file :as sut]
6 | [ventas.test-tools :as test-tools :refer [with-test-image]]))
7 |
8 | (def example-file
9 | {:file/keyword :example-file
10 | :file/extension "jpg"
11 | :schema/type :schema.type/file})
12 |
13 | (use-fixtures :once #(test-tools/with-test-context
14 | (entity/create* example-file)
15 | (%)))
16 |
17 | (deftest identifier
18 | (let [file (entity/find [:file/keyword (:file/keyword example-file)])]
19 | (is (= "example-file"
20 | (sut/identifier file)))
21 | (is (= (:db/id file)
22 | (sut/identifier (-> file
23 | (dissoc :file/keyword)))))))
24 |
25 | (deftest filename
26 | (let [file (entity/find [:file/keyword (:file/keyword example-file)])]
27 | (is (= "example-file.jpg" (sut/filename file)))))
28 |
29 | (deftest create-from-file!
30 | (with-test-image
31 | (fn [image]
32 | (is (= {:file/extension "png"
33 | :schema/type :schema.type/file}
34 | (-> (sut/create-from-file! (str image) "png")
35 | (dissoc :db/id)))))))
36 |
37 | (deftest normalization
38 | (let [file (entity/find [:file/keyword (:file/keyword example-file)])]
39 | (is (= "/files/example-file" (:url (entity/serialize file))))
40 | (is (not (:file/url (entity/deserialize :file (entity/serialize file)))))))
41 |
--------------------------------------------------------------------------------
/src/clj/ventas/entities/product_term.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.product-term
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [ventas.database.entity :as entity]
5 | [ventas.database.generators :as generators]
6 | [ventas.entities.i18n :as entities.i18n]))
7 |
8 | (spec/def :product.term/name ::entities.i18n/ref)
9 |
10 | (spec/def :product.term/keyword ::generators/keyword)
11 |
12 | (spec/def :product.term/taxonomy
13 | (spec/with-gen ::entity/ref
14 | #(entity/ref-generator :product.taxonomy)))
15 |
16 | (spec/def :schema.type/product.term
17 | (spec/keys :req [:product.term/name
18 | :product.term/taxonomy]
19 | :opt [:product.term/keyword]))
20 |
21 | (entity/register-type!
22 | :product.term
23 | {:migrations
24 | [[:base [{:db/ident :product.term/name
25 | :db/valueType :db.type/ref
26 | :db/cardinality :db.cardinality/one
27 | :db/isComponent true
28 | :ventas/refEntityType :i18n}
29 |
30 | {:db/ident :product.term/taxonomy
31 | :db/valueType :db.type/ref
32 | :db/cardinality :db.cardinality/one}
33 |
34 | {:db/ident :product.term/keyword
35 | :db/valueType :db.type/keyword
36 | :db/unique :db.unique/identity
37 | :db/cardinality :db.cardinality/one}]]
38 | [:missing-ref-entity-type [{:db/ident :product.term/taxonomy
39 | :ventas/refEntityType :product.taxonomy}]]]
40 |
41 | :dependencies
42 | #{:product.taxonomy :i18n}
43 |
44 | :autoresolve? true})
45 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/zoomable_image.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.zoomable-image
2 | (:require
3 | [js-image-zoom :as zoom]
4 | [re-frame.core :as rf]
5 | [reagent.core :as reagent]))
6 |
7 | (def state-key ::state)
8 |
9 | (rf/reg-event-db
10 | ::set-loaded
11 | (fn [db [_ id]]
12 | (assoc-in db [state-key id :loaded?] true)))
13 |
14 | (rf/reg-sub
15 | ::loaded?
16 | (fn [db [_ id]]
17 | (get-in db [state-key id :loaded?])))
18 |
19 | (defn- zoom-component [_ _ config]
20 | (let [image-zoom (atom nil)]
21 | (reagent/create-class
22 | {:component-will-unmount #(.kill @image-zoom)
23 | :display-name "zoom-component"
24 | :component-did-mount
25 | (fn [this]
26 | (reset! image-zoom (zoom. (reagent/dom-node this)
27 | (clj->js config))))
28 | :reagent-render (fn [id src _]
29 | [:div
30 | [:img {:src src
31 | :onLoad #(rf/dispatch [::set-loaded id])}]])})))
32 |
33 | (defn main-view [id size-kw zoomed-size-kw]
34 | {:pre [(keyword? size-kw)]}
35 | (let [size @(rf/subscribe [:db [:image-sizes size-kw]])
36 | loaded? @(rf/subscribe [::loaded? id])]
37 | (when size
38 | [:div.zoomable-image (when-not loaded? {:style {:position "absolute"
39 | :top -9999}})
40 | ^{:key (hash [loaded? id])}
41 | [zoom-component id
42 | (str "images/" id "/resize/" (name zoomed-size-kw))
43 | {:width (dec (:width size))
44 | :height (:height size)
45 | :scale 0.7}]])))
46 |
--------------------------------------------------------------------------------
/src/clj/ventas/entities/product_review.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.product-review
2 | (:require
3 | [ventas.database.entity :as entity]
4 | [ventas.database.generators :as generators]
5 | [clojure.spec.alpha :as spec]))
6 |
7 | (spec/def :product.review/title ::generators/string)
8 | (spec/def :product.review/user ::generators/string)
9 | (spec/def :product.review/content ::generators/string)
10 | (spec/def :product.review/rating double?)
11 |
12 | (spec/def :schema.type/product.review
13 | (spec/keys :req [:product.review/user
14 | :product.review/product]
15 | :opt [:product.review/title
16 | :product.review/content
17 | :product.review/rating]))
18 |
19 | (entity/register-type!
20 | :product.review
21 | {:migrations
22 | [[:base [{:db/ident :product.review/title
23 | :db/cardinality :db.cardinality/one
24 | :db/valueType :db.type/string}
25 | {:db/ident :product.review/content
26 | :db/cardinality :db.cardinality/one
27 | :db/valueType :db.type/string}
28 | {:db/ident :product.review/user
29 | :db/cardinality :db.cardinality/one
30 | :db/valueType :db.type/ref
31 | :ventas/refEntityType :user}
32 | {:db/ident :product.review/product
33 | :db/cardinality :db.cardinality/one
34 | :db/valueType :db.type/ref
35 | :ventas/refEntityType :product}
36 | {:db/ident :product.review/rating
37 | :db/cardinality :db.cardinality/one
38 | :db/valueType :db.type/float}]]]})
39 |
40 |
--------------------------------------------------------------------------------
/src/clj/ventas/system.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.system
2 | "Utilities for managing the state of the system"
3 | (:require
4 | [mount.core :as mount]
5 | [ventas.events :as events]
6 | [clojure.core.async :as core.async]))
7 |
8 | (def default-subsystem-mapping
9 | {:config '[ventas.config/config]
10 | :db '[ventas.database/conn]
11 | :storage '[ventas.storage/storage-backend]
12 | :es-client '[ventas.search/elasticsearch]
13 | :tx-processor '[ventas.database.tx-processor/tx-processor]
14 | :es-indexer '[ventas.search/indexer]
15 | :server '[ventas.server/server]})
16 |
17 | (def default-states
18 | (->> default-subsystem-mapping
19 | (vals)
20 | (map (comp (partial ns-resolve *ns*) first))))
21 |
22 | (defn get-states [subsystems & {:keys [mapping] :or {mapping default-subsystem-mapping}}]
23 | (->> subsystems
24 | (mapcat (fn [kw]
25 | (let [states (get mapping kw)]
26 | (when-not states
27 | (throw (Exception. (str "State " kw " does not exist"))))
28 | states)))
29 | (map #(ns-resolve *ns* %))))
30 |
31 | (defn r
32 | "Restarts subsystems. Example:
33 | (r :figwheel :db)
34 | This would restart figwheel and the database connection.
35 | Refer to keyword->state for the available subsystems."
36 | [& subsystems]
37 | (let [states (get-states subsystems)]
38 | (if (empty? states)
39 | :done
40 | (let [{:keys [stopped]} (apply mount/stop states)
41 | {:keys [started]} (apply mount/start states)]
42 | (core.async/put! (events/pub :init) true)
43 | {:stopped stopped
44 | :started started}))))
--------------------------------------------------------------------------------
/src/cljs/ventas/components/popover.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.popover
2 | "Use this when you want a component to appear when the user clicks
3 | somewhere, and you want it to disappear when the user clicks anywhere
4 | in the window (but outside of the component)"
5 | (:require
6 | [ventas.utils :as utils]
7 | [reagent.core :as reagent]
8 | [re-frame.core :as rf]))
9 |
10 | (def state-key ::state)
11 |
12 | (defn set-state [db target-id new-state]
13 | (assoc-in db [state-key :active-id] (when new-state target-id)))
14 |
15 | (rf/reg-event-db
16 | ::toggle
17 | (fn [db [_ id]]
18 | (let [{:keys [active-id]} (get db state-key)]
19 | (set-state db id (not (= active-id id))))))
20 |
21 | (rf/reg-event-db
22 | ::show
23 | (fn [db [_ id]]
24 | (set-state db id true)))
25 |
26 | (rf/reg-event-db
27 | ::hide
28 | (fn [db [_ id]]
29 | (set-state db id false)))
30 |
31 | (rf/reg-sub
32 | ::active?
33 | (fn [db [_ id]]
34 | (= (get-in db [state-key :active-id]) id)))
35 |
36 | (defn popover [id _]
37 | {:pre [id]}
38 | (let [node (atom nil)
39 | click-listener (fn [e]
40 | (when-not (utils/child? (.-target e) @node)
41 | (rf/dispatch [::hide id])))]
42 | (reagent/create-class
43 | {:component-will-unmount
44 | (fn [_]
45 | (.removeEventListener js/window "click" click-listener))
46 | :component-did-mount
47 | (fn [this]
48 | (reset! node (reagent/dom-node this))
49 | (.addEventListener js/window "click" click-listener))
50 | :reagent-render
51 | (fn [_ content]
52 | (when @(rf/subscribe [::active? id])
53 | content))})))
54 |
--------------------------------------------------------------------------------
/test/clj/ventas/email_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.email-test
2 | (:require
3 | [clojure.test :refer [deftest is use-fixtures]]
4 | [postal.core :as postal]
5 | [ventas.email :as sut]
6 | [ventas.email.templates :as templates]))
7 |
8 | (def test-configuration
9 | {:host "smtp.gmail.com"
10 | :port 443
11 | :user "no-reply@kazer.es"
12 | :pass "test"
13 | :from "no-reply@kazer.es"
14 | :ssl true})
15 |
16 | (use-fixtures :once
17 | #(with-redefs [sut/get-config (constantly test-configuration)]
18 | (%)))
19 |
20 | (deftest send!
21 | (let [received-args (atom nil)
22 | message {:subject "Hey!"
23 | :body "My message"}]
24 | (with-redefs [postal/send-message (fn [& args] (reset! received-args args))]
25 | (sut/send! message)
26 | (is (= [test-configuration
27 | (merge message
28 | {:from (:from test-configuration)})]
29 | @received-args)))))
30 |
31 | (deftest send-template!
32 | (let [received-args (atom nil)
33 | subject "Hey!"
34 | template "Test body"
35 | email "test@send-template.com"]
36 | (defmethod templates/template :test-template [_ _]
37 | {:body template
38 | :subject subject})
39 | (with-redefs [postal/send-message (fn [& args] (reset! received-args args))]
40 | (sut/send-template! :test-template {:user {:user/email email}})
41 | (is (= [test-configuration
42 | {:body [{:content template :type "text/html; charset=utf-8"}]
43 | :from (:from test-configuration)
44 | :subject subject
45 | :to email}]
46 | @received-args)))))
47 |
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/customization/menus.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.customization.menus
2 | (:require
3 | [ventas.routes :as routes]
4 | [ventas.themes.admin.skeleton :as admin.skeleton]
5 | [ventas.components.table :as table]
6 | [ventas.components.crud-table :as crud-table]
7 | [ventas.themes.admin.customization.menus.edit]
8 | [ventas.i18n :refer [i18n]]
9 | [re-frame.core :as rf]))
10 |
11 | (def state-path [::state :table])
12 |
13 | (defn- page []
14 | [admin.skeleton/skeleton
15 | [:div.admin__default-content.admin-menus__page
16 | [:div.admin-menus__table
17 | [table/table state-path]]]])
18 |
19 | (defn- items-column [data]
20 | [:p (->> (:items data)
21 | (map :name)
22 | (interpose ", ")
23 | (apply str))])
24 |
25 | (rf/reg-event-fx
26 | ::init
27 | (fn [_ _]
28 | {:dispatch [::crud-table/init state-path
29 | {:columns [{:id :name
30 | :label (i18n ::name)
31 | :component (partial table/link-column :admin.customization.menus.edit :id :name)}
32 | {:id :items
33 | :label (i18n ::items)
34 | :component items-column}
35 | (crud-table/action-column state-path)]
36 | :edit-route :admin.customization.menus.edit
37 | :entity-type :menu}]}))
38 |
39 | (admin.skeleton/add-menu-item!
40 | {:route :admin.customization.menus
41 | :parent :admin.customization
42 | :mobile? false
43 | :label ::page})
44 |
45 | (routes/define-route!
46 | :admin.customization.menus
47 | {:name ::page
48 | :url "menus"
49 | :component page
50 | :init-fx [::init]})
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/plugins.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.plugins
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.components.table :as table]
5 | [ventas.server.api.admin :as api.admin]
6 | [ventas.i18n :refer [i18n]]
7 | [ventas.themes.admin.skeleton :as admin.skeleton]
8 | [ventas.routes :as routes]))
9 |
10 | (def state-key ::state)
11 |
12 | (rf/reg-event-fx
13 | ::fetch
14 | (fn [{:keys [db]} [_ state-path]]
15 | (let [{:keys [page items-per-page sort-direction sort-column]} (table/get-state db state-path)]
16 | {:dispatch [::api.admin/admin.plugins.list
17 | {:success ::fetch.next
18 | :params {:pagination {:page page
19 | :items-per-page items-per-page}
20 | :sorting {:direction sort-direction
21 | :field sort-column}}}]})))
22 |
23 | (rf/reg-event-db
24 | ::fetch.next
25 | (fn [db [_ {:keys [items total]}]]
26 | (-> db
27 | (assoc-in [state-key :table :rows] items)
28 | (assoc-in [state-key :table :total] total))))
29 |
30 | (defn- content []
31 | [:div.admin-plugins__table
32 | [table/table [state-key :table]]])
33 |
34 | (defn- page []
35 | [admin.skeleton/skeleton
36 | [:div.admin__default-content.admin-plugins__page
37 | [content]]])
38 |
39 | (rf/reg-event-fx
40 | ::init
41 | (fn [_ _]
42 | {:dispatch [::table/init [state-key :table]
43 | {:fetch-fx [::fetch]
44 | :columns [{:id :name
45 | :label (i18n ::name)}]}]}))
46 |
47 | (routes/define-route!
48 | :admin.plugins
49 | {:name ::page
50 | :url "plugins"
51 | :component page
52 | :init-fx [::init]})
53 |
--------------------------------------------------------------------------------
/src/clj/ventas/config.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.config
2 | (:refer-clojure :exclude [get set])
3 | (:require
4 | [cprop.core :as cprop]
5 | [cprop.source]
6 | [mount.core :refer [defstate]]
7 | [ventas.common.utils :refer [deep-merge]]
8 | [ventas.utils :as utils]
9 | [clojure.tools.logging :as log]))
10 |
11 | (defn profile []
12 | (keyword (System/getenv "VENTAS_PROFILE")))
13 |
14 | (defn- prepare-config [config]
15 | (deep-merge (dissoc config :environments)
16 | (or (clojure.core/get (:environments config) (profile))
17 | {})))
18 |
19 | (defn- load-config []
20 | (let [custom-config (utils/swallow
21 | (cprop.source/from-resource "config.edn"))]
22 | (apply cprop/load-config
23 | :resource "ventas/config/base-config.edn"
24 | (when custom-config
25 | [:merge [(prepare-config custom-config)]]))))
26 |
27 | (defn start-config! []
28 | (log/info "Loading configuration")
29 | (let [{:keys [auth-secret] :as config-data} (load-config)]
30 | (log/debug config-data)
31 | (when (and (= :prod (profile)) (or (empty? auth-secret) (= auth-secret "CHANGEME")))
32 | (throw (Exception. (str ":auth-secret is empty or has not been changed.\n"
33 | "Either edit resources/config.edn or add an AUTH_SECRET environment variable, and try again."))))
34 | (atom config-data)))
35 |
36 | (defstate config :start (start-config!))
37 |
38 | (defn set
39 | [k-or-ks v]
40 | {:pre [(or (keyword? k-or-ks) (sequential? k-or-ks))]}
41 | (let [ks (if (keyword? k-or-ks)
42 | [k-or-ks]
43 | k-or-ks)]
44 | (swap! config assoc-in ks v)))
45 |
46 | (defn get [& ks]
47 | (get-in @config ks))
48 |
--------------------------------------------------------------------------------
/src/scss/main.scss:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | html {
4 | min-height: 100vh;
5 | }
6 |
7 | body {
8 | position: absolute;
9 | height: 100%;
10 | width: 100%;
11 | }
12 |
13 | #app {
14 | height: 100%;
15 | }
16 |
17 | .main {
18 | margin: 15px;
19 | }
20 |
21 | .menu.item {
22 | display: block;
23 | text-decoration: none;
24 | color: black;
25 | padding: 6px 10px;
26 | }
27 |
28 | .centered-segment-wrapper {
29 | display: flex;
30 | align-items: center;
31 | min-height: 100vh;
32 | }
33 |
34 | .centered-segment {
35 | width: 100%;
36 | text-align: center;
37 |
38 | .ui.segment {
39 | display: inline-block;
40 | text-align: left;
41 | }
42 | }
43 |
44 | body.animating.in.dimmable, body.dimmed.dimmable {
45 | overflow: visible !important;
46 | }
47 |
48 | @import "./ventas/components/base";
49 | @import "./ventas/components/category-list";
50 | @import "./ventas/components/cart";
51 | @import "./ventas/components/colorpicker";
52 | @import "./ventas/components/datepicker";
53 | @import "./ventas/components/draggable-list";
54 | @import "./ventas/components/error";
55 | @import "./ventas/components/image";
56 | @import "./ventas/components/image-input";
57 | @import "./ventas/components/i18n-input";
58 | @import "./ventas/components/popup";
59 | @import "./ventas/components/product-list";
60 | @import "./ventas/components/product-filters";
61 | @import "./ventas/components/notificator";
62 | @import "./ventas/components/cookies";
63 | @import "./ventas/components/menu";
64 | @import "./ventas/components/search-box";
65 | @import "./ventas/components/sidebar";
66 | @import "./ventas/components/table";
67 | @import "./ventas/components/term";
68 | @import "./ventas/components/breadcrumbs";
69 | @import "./ventas/pages/admin";
--------------------------------------------------------------------------------
/src/clj/ventas/core.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.core
2 | (:refer-clojure :exclude [reset!])
3 | (:require
4 | [clojure.tools.logging :as log]
5 | [clojure.core.async :as core.async :refer [>! go]]
6 | [mount.core :as mount]
7 | [ventas.config :as config]
8 | [ventas.database :as db]
9 | [ventas.database.schema :as schema]
10 | [ventas.database.seed :as seed]
11 | [ventas.database.tx-processor :as tx-processor]
12 | [ventas.email.templates.core]
13 | [ventas.entities.core]
14 | [ventas.events :as events]
15 | [ventas.i18n.cldr :as cldr]
16 | [ventas.search :as search]
17 | [ventas.search.indexing]
18 | [ventas.server :as server]
19 | [ventas.server.api.core]
20 | [ventas.entities.file :as entities.file])
21 | (:gen-class))
22 |
23 | (defn start-system! [& [states]]
24 | (let [states (or states [#'config/config
25 | #'db/conn
26 | #'search/elasticsearch
27 | #'search/indexer
28 | #'tx-processor/tx-processor
29 | #'server/server])]
30 | (apply mount/start states)
31 | (core.async/put! (events/pub :init) true)))
32 |
33 | (defn -main [& args]
34 | (log/info "Starting up...")
35 | (start-system!))
36 |
37 | (defn resource-as-stream [s]
38 | (let [loader (.getContextClassLoader (Thread/currentThread))]
39 | (.getResourceAsStream loader s)))
40 |
41 | (defn setup!
42 | "- Migrates the db
43 | - Imports CLDR data
44 | - Transacts entity and plugin fixtures
45 | - Sets the ventas logo as the logo"
46 | [& {:keys [recreate?]}]
47 | (schema/migrate :recreate? recreate?)
48 | (seed/seed)
49 | (cldr/import-cldr!)
50 | (entities.file/create-from-file! (resource-as-stream "ventas/logo.png") "png" :logo)
51 | :done)
--------------------------------------------------------------------------------
/src/clj/ventas/entities/amount.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.amount
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [ventas.database.entity :as entity]
5 | [ventas.database.generators :as generators]
6 | [ventas.utils :as utils]
7 | [ventas.search.indexing :as search.indexing]
8 | [ventas.search :as search]))
9 |
10 | (spec/def :amount/keyword ::generators/keyword)
11 |
12 | (spec/def :amount/value ::generators/bigdec)
13 |
14 | (spec/def :amount/currency
15 | (spec/with-gen ::entity/ref #(entity/ref-generator :currency)))
16 |
17 | (spec/def :schema.type/amount
18 | (spec/keys :req [:amount/value
19 | :amount/currency]
20 | :opt [:amount/keyword]))
21 |
22 | (entity/register-type!
23 | :amount
24 | {:migrations
25 | [[:base [{:db/ident :amount/keyword
26 | :db/valueType :db.type/keyword
27 | :db/cardinality :db.cardinality/one
28 | :db/unique :db.unique/identity}
29 | {:db/ident :amount/value
30 | :db/valueType :db.type/bigdec
31 | :db/cardinality :db.cardinality/one
32 | :db/index true}
33 | {:db/ident :amount/currency
34 | :db/valueType :db.type/ref
35 | :db/cardinality :db.cardinality/one}]]]
36 |
37 | :dependencies #{:currency}
38 | :seed-number 0
39 | :autoresolve? true})
40 |
41 | (defmethod search.indexing/transform-entity-by-type :amount [entity]
42 | (:amount/value entity))
43 |
44 | (defn ->entity
45 | "Creates an amount entity from the given parameters. Meant for quick creation
46 | of amount entities"
47 | [amount currency-kw]
48 | {:pre [(utils/bigdec? amount) (keyword? currency-kw)]}
49 | {:schema/type :schema.type/amount
50 | :amount/currency [:currency/keyword currency-kw]
51 | :amount/value amount})
52 |
--------------------------------------------------------------------------------
/src/clj/ventas/entities/country.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.country
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [ventas.database.entity :as entity]
5 | [ventas.entities.i18n :as entities.i18n]))
6 |
7 | (spec/def :schema.type/country.group
8 | (spec/keys :req [:country.group/name]))
9 |
10 | (entity/register-type!
11 | :country.group
12 | {:migrations
13 | [[:base [{:db/ident :country.group/name
14 | :db/valueType :db.type/ref
15 | :db/cardinality :db.cardinality/one}
16 | {:db/ident :country.group/keyword
17 | :db/valueType :db.type/keyword
18 | :db/unique :db.unique/identity
19 | :db/cardinality :db.cardinality/one}]]
20 | [:add-missing-is-component [{:db/ident :country.group/name
21 | :db/valueType :db.type/ref
22 | :db/isComponent true
23 | :db/cardinality :db.cardinality/one}]]]
24 |
25 | :dependencies
26 | #{:i18n}
27 |
28 | :autoresolve? true})
29 |
30 | (spec/def :country/name ::entities.i18n/ref)
31 |
32 | (spec/def :schema.type/country
33 | (spec/keys :req [:country/name]))
34 |
35 | (entity/register-type!
36 | :country
37 | {:migrations
38 | [[:base [{:db/ident :country/name
39 | :db/valueType :db.type/ref
40 | :db/cardinality :db.cardinality/one
41 | :db/isComponent true
42 | :ventas/refEntityType :i18n}
43 | {:db/ident :country/keyword
44 | :db/valueType :db.type/keyword
45 | :db/unique :db.unique/identity
46 | :db/cardinality :db.cardinality/one}
47 | {:db/ident :country/group
48 | :db/valueType :db.type/ref
49 | :db/cardinality :db.cardinality/one
50 | :ventas/refEntityType :country.group}]]]
51 |
52 | :dependencies
53 | #{:i18n :country.group}
54 |
55 | :autoresolve? true})
56 |
--------------------------------------------------------------------------------
/src/clj/ventas/email/elements.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.email.elements
2 | (:require
3 | [clojure.java.io :as io]
4 | [ventas.config :as config]
5 | [ventas.entities.configuration :as entities.configuration]
6 | [ventas.entities.user :as entities.user]
7 | [ventas.i18n :refer [i18n]]
8 | [ventas.entities.i18n :as entities.i18n]))
9 |
10 | (defn get-url [s]
11 | (let [{:keys [host port]} (config/get :server)]
12 | (str "http://" host ":" port s)))
13 |
14 | (defn table [args & content]
15 | [:table (merge {:cellSpacing 0
16 | :cellPadding 0
17 | :border 0
18 | :style {:border "0px"}}
19 | args)
20 | [:tbody
21 | content]])
22 |
23 | (defn wrapper [& content]
24 | [:html {:xmlns "http://www.w3.org/1999/xhtml"}
25 | [:head
26 | [:style {:type "text/css"}
27 | (slurp (io/resource "ventas/email/elements/email.css"))]]
28 | [:body
29 | (table
30 | {:width "100%"
31 | :height "100%"
32 | :id "bodyTable"}
33 | [:tr
34 | [:td
35 | (table
36 | {:width 600
37 | :cellPadding 20
38 | :id "emailContainer"}
39 | [:tr
40 | [:td {:align "center"
41 | :style {:text-align "center"}}
42 | content]])]])]])
43 |
44 | (defn header [attrs name]
45 | [:th.table-header attrs
46 | name])
47 |
48 | (defn skeleton [user & content]
49 | (let [culture-kw (entities.i18n/culture->kw
50 | (entities.user/get-culture user))]
51 | [:div
52 | (wrapper
53 | (table
54 | {:width "100%"}
55 | [:tr
56 | [:td.logo {:align "center"
57 | :style {:text-align "center"}}
58 | [:img {:src "/files/logo"}]
59 | [:h2 (entities.configuration/get :customization/name)]]]
60 | [:tr
61 | [:td {:style {:text-align "left"}
62 | :align "left"}
63 | [:h3 (i18n culture-kw ::hello (entities.user/get-name (:db/id user)))]
64 | content]]))]))
65 |
--------------------------------------------------------------------------------
/src/clj/ventas/entities/tax.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.tax
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [clojure.test.check.generators :as gen]
5 | [ventas.database :as db]
6 | [ventas.database.entity :as entity]
7 | [ventas.database.generators :as generators]
8 | [ventas.entities.i18n :as entities.i18n]
9 | [ventas.utils :as utils]))
10 |
11 | (spec/def :tax/name ::entities.i18n/ref)
12 |
13 | (def kinds
14 | #{:tax.kind/percentage
15 | :tax.kind/amount})
16 |
17 | (spec/def :tax/kind
18 | (spec/with-gen
19 | (spec/or :pull-eid ::db/pull-eid
20 | :kind kinds)
21 | #(gen/elements kinds)))
22 |
23 | (spec/def :tax/amount
24 | (spec/with-gen ::entity/ref
25 | #(entity/ref-generator :amount)))
26 |
27 | (spec/def :tax/keyword ::generators/keyword)
28 |
29 | (spec/def :schema.type/tax
30 | (spec/keys :req [:tax/name
31 | :tax/kind
32 | :tax/amount]
33 | :opt [:tax/keyword]))
34 |
35 | (entity/register-type!
36 | :tax
37 | {:migrations
38 | [[:base (utils/into-n
39 | [{:db/ident :tax/name
40 | :db/valueType :db.type/ref
41 | :db/cardinality :db.cardinality/one
42 | :db/isComponent true
43 | :ventas/refEntityType :i18n}
44 |
45 | {:db/ident :tax/keyword
46 | :db/valueType :db.type/keyword
47 | :db/unique :db.unique/identity
48 | :db/cardinality :db.cardinality/one}
49 |
50 | {:db/ident :tax/amount
51 | :db/valueType :db.type/ref
52 | :db/cardinality :db.cardinality/one
53 | :db/isComponent true
54 | :ventas/refEntityType :amount}
55 |
56 | {:db/ident :tax/kind
57 | :db/valueType :db.type/ref
58 | :db/cardinality :db.cardinality/one
59 | :ventas/refEntityType :enum}]
60 |
61 | (map #(hash-map :db/ident %) kinds))]]
62 |
63 | :autoresolve? true
64 |
65 | :dependencies
66 | #{:i18n}})
67 |
--------------------------------------------------------------------------------
/src/clj/ventas/site.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.site
2 | (:require
3 | [mount.core :refer [defstate]]
4 | [datomic.api :as d]
5 | [ventas.database :as db]
6 | [clojure.string :as str]
7 | [ventas.database.entity :as entity])
8 | (:import [datomic Datom]))
9 |
10 | (def ^:dynamic *current* nil)
11 |
12 | (def shared-types
13 | #{:schema.type/country
14 | :schema.type/state
15 | :schema.type/site
16 | :schema.type/currency
17 | :schema.type/i18n.culture})
18 |
19 | (defn site-db [site]
20 | (d/filter (d/db db/conn)
21 | (fn [_ ^Datom datom]
22 | (let [entity (d/entity (d/db db/conn) (.e datom))
23 | entity-site (get-in entity [:ventas/site :db/id])
24 | type (get-in entity [:schema/type])]
25 | (or (contains? shared-types type)
26 | (= entity-site site))))))
27 |
28 | (defn by-hostname [hostname]
29 | (when-let [subdomain (some-> hostname
30 | (str/split #"\.")
31 | first)]
32 | (entity/query-one :site {:subdomain subdomain})))
33 |
34 | (defn transact
35 | "Adds :ventas/site if needed"
36 | [items]
37 | (db/transact*
38 | (map (fn [item]
39 | (if-not (map? item)
40 | item
41 | (let [type (:schema/type item)
42 | ref (db/normalize-ref type)
43 | ident (db/ident ref)]
44 | (if (or (not *current*) (contains? shared-types ident))
45 | item
46 | (assoc item :ventas/site *current*)))))
47 | items)))
48 |
49 | (defn with-site [site-id f]
50 | (with-bindings {#'db/db #(site-db site-id)
51 | #'*current* site-id
52 | #'db/transact transact}
53 | (f)))
54 |
55 | (defn with-hostname [hostname f]
56 | (if-let [site (by-hostname hostname)]
57 | (with-site (:db/id site) f)
58 | (f)))
59 |
60 | (defn wrap-multisite
61 | [handler]
62 | (fn [{:keys [server-name] :as req}]
63 | (with-hostname server-name #(handler req))))
--------------------------------------------------------------------------------
/test/clj/ventas/server/ws_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.server.ws-test
2 | (:require
3 | [chord.channels :refer [bidi-ch]]
4 | [clojure.core.async :as core.async :refer [! go]]
5 | [clojure.test :refer [deftest is testing use-fixtures]]
6 | [ventas.server.ws :as sut]
7 | [ventas.utils :as utils]))
8 |
9 | (use-fixtures :once #(do (sut/start!)
10 | (%)))
11 |
12 | (deftest call-handler-with-user
13 | (defmethod sut/handle-request :call-handler-with-user-test [request {:keys [session]}]
14 | (is (= {:name :call-handler-with-user-test
15 | :throw? true
16 | :params {:some :data}}
17 | request))
18 | (is (= @session {:user 1})))
19 | (sut/call-handler-with-user :call-handler-with-user-test
20 | {:some :data}
21 | {:schema/type :schema.type/user
22 | :db/id 1}))
23 |
24 | (deftest handle-json-messages
25 | (let [test-message {:name :handle-json-messages-test
26 | :id :test-request
27 | :params [:test :data]
28 | :type :request}]
29 | (defmethod sut/handle-request :handle-json-messages-test [message {:keys [channel client-id request session]}]
30 | (is (= test-message message))
31 | (is (utils/chan? channel))
32 | (is client-id)
33 | (is (= [:ws-channel] (keys request)))
34 | (is (= @session {})))
35 |
36 | (let [read-ch (core.async/chan)
37 | write-ch (core.async/chan)
38 | ch (go
39 | (sut/handle-messages :transit-json {} {:ws-channel (bidi-ch read-ch write-ch)})
40 | (>! read-ch {:message test-message})
41 | (is (= {:data true
42 | :id :test-request
43 | :realtime? false
44 | :success true
45 | :channel-key nil
46 | :type :response}
47 | (user token)]
27 | (server.api/set-user session user)))
28 | (try
29 | (response
30 | content-type
31 | {:status 200
32 | :body (server.ws/handle-request body
33 | {:session session})})
34 | (catch Throwable e
35 | (let [type (:type (ex-data e))
36 | content-type (if (= type ::unsupported-content-type)
37 | "application/edn"
38 | content-type)]
39 | (response
40 | content-type
41 | {:status (case type
42 | ::server.ws/api-call-not-found 404
43 | ::server.api.user/authentication-required 401
44 | ::utils/spec-invalid 400
45 | ::unsupported-content-type 400
46 | 500)
47 | :body (server.ws/exception->message e)}))))))
--------------------------------------------------------------------------------
/src/cljs/ventas/components/datepicker.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.datepicker
2 | (:require
3 | [react-date-range]
4 | [re-frame.core :as rf]
5 | [reagent.ratom :refer [atom]]
6 | [ventas.components.base :as base]
7 | [ventas.utils :as utils]
8 | [reagent.core :as reagent]))
9 |
10 | (def state-key ::state)
11 |
12 | (rf/reg-event-fx
13 | ::set-value
14 | (fn [_ [_ id on-change-fx {:strs [startDate endDate]}]]
15 | {:dispatch-n
16 | [[:db [state-key id] (str (.format startDate "YYYY-MM-DD")
17 | " / "
18 | (.format endDate "YYYY-MM-DD"))]
19 | (conj on-change-fx {:start startDate
20 | :end endDate})]}))
21 |
22 | (defn range-picker [{:keys [on-change-fx]}]
23 | (js/React.createElement
24 | js/ReactDateRange.DateRange
25 | #js {:onChange #(rf/dispatch (conj on-change-fx (js->clj %)))}))
26 |
27 | (defn- handle-input-click [this-node focused? target]
28 | (when-not (utils/child? target this-node)
29 | (reset! focused? false)))
30 |
31 | (defn range-input [_]
32 | (let [focused? (atom false)
33 | id (str (gensym))
34 | node (atom nil)
35 | click-listener #(handle-input-click @node focused? (.-target %))]
36 | (reagent/create-class
37 | {:component-will-unmount
38 | (fn [_]
39 | (.removeEventListener js/window "click" click-listener))
40 | :component-did-mount
41 | (fn [this]
42 | (reset! node (reagent/dom-node this))
43 | (.addEventListener js/window "click" click-listener))
44 | :reagent-render
45 | (fn [{:keys [placeholder on-change-fx]}]
46 | [:div.date-range-input
47 | [base/input
48 | [:input {:on-focus #(reset! focused? true)
49 | :readOnly true
50 | :value (or @(rf/subscribe [:db [state-key id]])
51 | "")
52 | :placeholder placeholder}]]
53 | (when @focused?
54 | [range-picker {:on-change-fx [::set-value id on-change-fx]}])])})))
55 |
56 |
--------------------------------------------------------------------------------
/src/clj/ventas/search/schema.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.search.schema
2 | (:require
3 | [ventas.database.entity :as entity]
4 | [ventas.search :as search]
5 | [ventas.database :as db]
6 | [ventas.utils :as utils]
7 | [clojure.string :as str]))
8 |
9 | (defn- find-migrated-ids []
10 | (-> (search/search {:query {:term {:schema/type :es-migration}}})
11 | (get-in [:body :hits :hits])
12 | (->> (map (fn [hit]
13 | (-> hit :_source :es-migration/keyword (subs 1) keyword))))
14 | (set)))
15 |
16 | (defn pending-migrations []
17 | (let [migrated-ids (find-migrated-ids)]
18 | (->> @search/type-config
19 | (mapcat (fn [[kw config]]
20 | (map (fn [[migr-kw migr]]
21 | [(keyword (name kw) (name migr-kw)) migr])
22 | (:migrations config))))
23 | (remove (comp migrated-ids first)))))
24 |
25 | (defn migrate! []
26 | (doseq [[kw migr] (pending-migrations)]
27 | (when (:properties migr)
28 | (search/update-index
29 | {:properties (:properties migr)}))
30 | (search/create-document {:schema/type :es-migration
31 | :es-migration/keyword kw})))
32 |
33 | (defn autocomplete-type []
34 | {:type "text"
35 | :analyzer "ventas/autocomplete"
36 | :search_analyzer "standard"})
37 |
38 | (defn setup! []
39 | (search/create-index
40 | {:mappings {:doc {:properties {:es-migration/keyword {:type "keyword"}
41 | :schema/type {:type "keyword"}}}}
42 | :settings {:analysis {:filter {:autocomplete_filter {:type "edge_ngram"
43 | :min_gram 1
44 | :max_gram 20}}
45 | :analyzer {:ventas/autocomplete {:type "custom"
46 | :tokenizer "standard"
47 | :filter ["lowercase"
48 | "autocomplete_filter"]}}}}})
49 | (migrate!))
50 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/crud_form.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.crud-form
2 | "Implementation of ventas.components.form for the most boring case"
3 | (:require
4 | [re-frame.core :as rf]
5 | [ventas.routes :as routes]
6 | [ventas.i18n :refer [i18n]]
7 | [ventas.server.api.admin :as api.admin]
8 | [ventas.components.notificator :as notificator]
9 | [ventas.components.base :as base]
10 | [ventas.utils.ui :as utils.ui]
11 | [ventas.components.form :as form]
12 | [ventas.i18n :as i18n])
13 | (:require-macros
14 | [ventas.utils :refer [ns-kw]]
15 | [ventas.components.crud-form]))
16 |
17 | (def state-key ::state)
18 |
19 | (rf/reg-event-fx
20 | ::submit
21 | (fn [{:keys [db]} [_ state-path list-route]]
22 | {:dispatch [::api.admin/admin.entities.save
23 | {:params (get-in db (conj state-path :form))
24 | :success [::submit.next list-route]}]}))
25 |
26 | (rf/reg-event-fx
27 | ::submit.next
28 | (fn [_ [_ list-route]]
29 | {:dispatch [::notificator/notify-saved]
30 | :go-to [list-route]}))
31 |
32 | (rf/reg-event-fx
33 | ::init
34 | (fn [_ [_ state-path entity-type next-event]]
35 | {:dispatch (let [id (routes/ref-from-param :id)]
36 | (if-not (pos? id)
37 | [::form/populate state-path {:schema/type (keyword "schema.type" (name entity-type))}]
38 | [::api.admin/admin.entities.pull
39 | {:params {:id id}
40 | :success [::init.next state-path next-event]}]))}))
41 |
42 | (rf/reg-event-fx
43 | ::init.next
44 | (fn [_ [_ state-path next-event data]]
45 | {:dispatch-n [[::form/populate state-path data]
46 | (when next-event
47 | (conj next-event data))]}))
48 |
49 | (defn component [state-path list-route component]
50 | [form/form state-path
51 | [base/form {:on-submit (utils.ui/with-handler
52 | #(rf/dispatch [::submit state-path list-route]))}
53 | component
54 | [base/form-button {:type "submit"}
55 | (i18n ::submit)]]])
56 |
57 | (i18n/register-translations!
58 | {:en_US {::submit "Submit"}
59 | :es_ES {::submit "Enviar"}})
--------------------------------------------------------------------------------
/src/cljs/ventas/components/notificator.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.notificator
2 | (:require
3 | [cljs.core.async :refer [\n"
27 | (hiccup/html
28 | [:html
29 | [:head
30 | [:base {:href "/"}]
31 | [:meta {:charset "UTF-8"}]
32 | [:title "ventas administration"]
33 | [:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
34 | [:link {:href "https://cdn.jsdelivr.net/npm/semantic-ui@2.2.14/dist/semantic.min.css"
35 | :rel "stylesheet"
36 | :type "text/css"}]
37 | (html/html-resources req)]
38 | [:body
39 | [:div#app]
40 | [:script "ventas.core.start();"]]])))
41 |
42 | (defn handle [req]
43 | {:status 200
44 | :headers {"Content-Type" "text/html; charset=utf-8"}
45 | :body (get-html req)})
46 |
47 | (defn css-middleware [handler]
48 | (fn [req]
49 | (handler
50 | (-> req
51 | (html/enqueue-css ::normalize "https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css")
52 | (html/enqueue-css ::base "/files/css/main.css")))))
53 |
54 | (defn js-middleware [handler]
55 | (fn [req]
56 | (handler
57 | (-> req
58 | (html/enqueue-js ::base "/files/js/admin/main.js")))))
59 |
60 | (def handler
61 | (-> (compojure/routes
62 | (GET "/admin*" _ handle))
63 | (css-middleware)
64 | (js-middleware)))
65 |
66 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/draggable_list.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.draggable-list
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.utils.ui :as utils.ui]))
5 |
6 | (def state-key ::state)
7 |
8 | (defn- put-before [items pos item]
9 | (let [items (remove #{item} items)
10 | head (take pos items)
11 | tail (drop pos items)]
12 | (concat head [item] tail)))
13 |
14 | (rf/reg-event-db
15 | ::on-drag-over
16 | (fn [db [_ id order position]]
17 | (let [{:keys [drag-index]} (get-in db [state-key id])]
18 | (if-not drag-index
19 | db
20 | (-> db
21 | (assoc-in [state-key id :hover-index] position)
22 | (assoc-in [state-key id :temp-order] (put-before order position drag-index)))))))
23 |
24 | (defn main-view [{:keys [id]} _]
25 | (let [id (or id (gensym))]
26 | (fn [{:keys [on-reorder on-drag-over props-fn]} items]
27 | (let [{:keys [temp-order drag-index hover-index]} @(rf/subscribe [:db [state-key id]])
28 | items (vec items)
29 | base-order (range (count items))
30 | order (or temp-order base-order)]
31 | [:ul.draggable-list
32 | (for [[idx position] (zipmap order (range))]
33 | [:li.draggable-list__item
34 | (merge
35 | {:key idx
36 | :class (condp = idx
37 | drag-index "draggable-list__item--active"
38 | hover-index "draggable-list__item--hovered"
39 | nil)
40 | :draggable true
41 | :on-drag-start #(rf/dispatch [:db [state-key id :drag-index] idx])
42 | :on-drag-over (utils.ui/with-handler
43 | #(if on-drag-over
44 | (on-drag-over id order position)
45 | (rf/dispatch [::on-drag-over id order position])))
46 | :on-drag-end (fn []
47 | (rf/dispatch [:db [state-key id] {}])
48 | (when on-reorder
49 | (on-reorder (map items order))))}
50 | (when props-fn (props-fn idx)))
51 | (get items idx)])]))))
52 |
--------------------------------------------------------------------------------
/test/clj/ventas/database/entity_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.database.entity-test
2 | (:require
3 | [clojure.test :refer [deftest is testing use-fixtures]]
4 | [ventas.database.entity :as sut]
5 | [ventas.test-tools :as test-tools]
6 | [ventas.entities.user :as user]
7 | [ventas.database :as db]))
8 |
9 | (use-fixtures :each #(test-tools/with-test-context
10 | (%)))
11 |
12 | (deftest entity?
13 | (is (= (sut/entity? {:schema/type :any-kw}) true))
14 | (is (= (sut/entity? {}) false))
15 | (is (= (sut/entity? {:type :any-kw}) false)))
16 |
17 | (def test-user-attrs
18 | {:first-name "Test user"
19 | :password ""
20 | :email "email@email.com"
21 | :status :user.status/active})
22 |
23 | (deftest entity-create
24 | (let [user (sut/create :user test-user-attrs)]
25 | (is (= (-> user
26 | (dissoc :db/id)
27 | (dissoc :user/password))
28 | {:schema/type :schema.type/user
29 | :user/first-name "Test user"
30 | :user/email "email@email.com"
31 | :user/status :user.status/active
32 | :user/culture (:db/id (db/entity user/default-culture))}))
33 | (sut/delete (:db/id user))))
34 |
35 | (deftest register-type!
36 | (sut/register-type! :new-type {:attributes []})
37 | (is (= (:new-type (sut/types)) {:attributes []}))
38 | (let [properties {:filter-json (fn [])
39 | :attributes []}]
40 | (sut/register-type! :new-type properties)
41 | (is (= (:new-type (sut/types)) properties))))
42 |
43 | (deftest entities-remove
44 | (let [{id :db/id} (sut/create :user test-user-attrs)]
45 | (sut/delete id)
46 | (is (not (sut/find id)))))
47 |
48 | (deftest entities-find
49 | (let [{id :db/id :as user} (sut/create :user test-user-attrs)]
50 | (is (= user (sut/find id)))
51 | (sut/delete id)))
52 |
53 | (deftest enum-retractions
54 | (let [{:db/keys [id]} (sut/create :user {:favorites [17592186045648 17592186045679 17592186045691]})]
55 | (is (= [[:db/retract id :user/favorites 17592186045691]
56 | [:db/retract id :user/favorites 17592186045679]]
57 | (#'sut/get-retractions
58 | {:db/id id
59 | :user/favorites [17592186045648]})))))
60 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/amount_input.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.amount-input
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.common.utils :as common.utils]
5 | [ventas.components.base :as base]))
6 |
7 | (rf/reg-event-fx
8 | ::set-field
9 | (fn [_ [_ amount ks v on-change-fx]]
10 | {:dispatch (conj on-change-fx
11 | (-> amount
12 | (assoc :schema/type :schema.type/amount)
13 | (assoc-in ks v)))}))
14 |
15 | (rf/reg-event-fx
16 | ::set-currency
17 | (fn [_ [_ amount v on-change-fx]]
18 | {:dispatch [::set-field amount [:amount/currency :db/id] v on-change-fx]}))
19 |
20 | (rf/reg-event-fx
21 | ::set-value
22 | (fn [_ [_ amount v on-change-fx]]
23 | {:dispatch [::set-field amount [:amount/value] v on-change-fx]}))
24 |
25 | (rf/reg-sub
26 | ::default-currency
27 | :<- [:db [:admin :general-config]]
28 | :<- [:db [:admin :currencies]]
29 | (fn [[{:general-config/keys [culture]} currencies]]
30 | (or (->> currencies (filter #(= (-> % :culture :id) culture)) first :id)
31 | (:id (first currencies)))))
32 |
33 | (defn- dropdown [{:keys [amount on-change-fx]}]
34 | (let [default-currency @(rf/subscribe [::default-currency])]
35 | ^{:key default-currency}
36 | [:div.amount-input__currency
37 | [base/form-field
38 | [base/dropdown
39 | {:fluid true
40 | :selection true
41 | :options (map (fn [v]
42 | {:text (:symbol v)
43 | :value (:id v)})
44 | @(rf/subscribe [:db [:admin :currencies]]))
45 | :default-value (or (get-in amount [:amount/currency :db/id])
46 | default-currency)
47 | :on-change #(rf/dispatch [::set-currency amount (js/parseFloat (.-value %2)) on-change-fx])}]]]))
48 |
49 | (defn input [{:keys [amount control label on-change-fx]}]
50 | [base/form-input {:label label :key (boolean amount)}
51 | [dropdown {:amount amount
52 | :on-change-fx on-change-fx}]
53 | [(or control :input)
54 | {:default-value (common.utils/bigdec->str (get amount :amount/value))
55 | :on-change #(rf/dispatch [::set-value amount (common.utils/str->bigdec (-> % .-target .-value)) on-change-fx])}]])
56 |
--------------------------------------------------------------------------------
/dev/clj/repl.clj:
--------------------------------------------------------------------------------
1 | (ns repl
2 | (:require
3 | [clojure.tools.namespace.repl :as tn]
4 | [mount.core :as mount]
5 | [ventas.system :as system]
6 | [ventas.server :as server]
7 | [compojure.core :refer [GET defroutes]]
8 | [shadow.cljs.devtools.server :as shadow.server]
9 | [shadow.cljs.devtools.api :as shadow.api]
10 | [hiccup.core :as hiccup]
11 | [ventas.html :as html]))
12 |
13 | (defn- devcards-html [req]
14 | (str "\n"
15 | (hiccup/html
16 | [:html
17 | [:head
18 | [:base {:href "/"}]
19 | [:meta {:charset "UTF-8"}]
20 | [:title "Devcards"]
21 | [:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
22 | [:script {:src "files/js/devcards/main.js"}]
23 | (html/html-resources req)]
24 | [:body
25 | [:div#app]
26 | [:script "devcards.core.start_devcard_ui_BANG__STAR_();"]]])))
27 |
28 | (defn handle-devcards [req]
29 | {:status 200
30 | :headers {"Content-Type" "text/html; charset=utf-8"}
31 | :body (devcards-html req)})
32 |
33 | (defroutes dev-routes
34 | (GET "/devcards" _
35 | handle-devcards))
36 |
37 | (defn refresh [& {:keys [after]}]
38 | (let [result (tn/refresh :after after)]
39 | (when (instance? Throwable result)
40 | (throw result))))
41 |
42 | (defn refresh-all [& {:keys [after]}]
43 | (let [result (tn/refresh-all :after after)]
44 | (when (instance? Throwable result)
45 | (throw result))))
46 |
47 | (alter-var-root #'*warn-on-reflection* (constantly true))
48 |
49 | (defn start [& [states]]
50 | (-> (mount/only (or states system/default-states))
51 | (mount/with-args {::server/handler #'dev-routes})
52 | mount/start)
53 | :done)
54 |
55 | (defn r [& subsystems]
56 | (let [states (system/get-states subsystems)]
57 | (when (seq states)
58 | (mount/stop states))
59 | (refresh :after 'repl/start)))
60 |
61 | (defn init-next []
62 | (start))
63 |
64 | (defn init []
65 | (require 'ventas.core)
66 | (refresh-all :after 'repl/init-next))
67 |
68 | (defn watch-cljs [build-id]
69 | (shadow.server/start!)
70 | (shadow.api/watch build-id))
71 |
72 | (defn release-cljs [build-id]
73 | (shadow.server/start!)
74 | (shadow.api/release build-id))
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/activity_log.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.activity-log
2 | (:require
3 | [clojure.string :as str]
4 | [re-frame.core :as rf]
5 | [ventas.components.table :as table]
6 | [ventas.server.api.admin :as api.admin]
7 | [ventas.i18n :refer [i18n]]
8 | [ventas.themes.admin.skeleton :as admin.skeleton]
9 | [ventas.routes :as routes]))
10 |
11 | (def state-key ::state)
12 |
13 | (rf/reg-event-fx
14 | ::fetch
15 | (fn [{:keys [db]} [_ state-path]]
16 | (let [{:keys [page items-per-page sort-direction sort-column]} (table/get-state db state-path)]
17 | {:dispatch [::api.admin/admin.events.list
18 | {:success ::fetch.next
19 | :params {:pagination {:page page
20 | :items-per-page items-per-page}
21 | :sorting {:direction sort-direction
22 | :field sort-column}}}]})))
23 |
24 | (rf/reg-event-db
25 | ::fetch.next
26 | (fn [db [_ {:keys [items total]}]]
27 | (-> db
28 | (assoc-in [state-key :table :rows] items)
29 | (assoc-in [state-key :table :total] total))))
30 |
31 | (defn- type-column [{:keys [type]}]
32 | [:span (:name type)])
33 |
34 | (defn- entity-type-column [{:keys [entity-type]}]
35 | [:span (str/capitalize (name entity-type))])
36 |
37 | (defn- content []
38 | [:div.admin-events__table
39 | [table/table [state-key :table]]])
40 |
41 | (defn- page []
42 | [admin.skeleton/skeleton
43 | [:div.admin__default-content.admin-events__page
44 | [content]]])
45 |
46 | (rf/reg-event-fx
47 | ::init
48 | (fn [_ _]
49 | {:dispatch [::table/init [state-key :table]
50 | {:fetch-fx [::fetch]
51 | :columns [{:id :entity-id
52 | :label (i18n ::entity-id)}
53 | {:id :entity-type
54 | :label (i18n ::entity-type)
55 | :component entity-type-column}
56 | {:id :type
57 | :label (i18n ::type)
58 | :component type-column}]}]}))
59 |
60 | (routes/define-route!
61 | :admin.activity-log
62 | {:name ::page
63 | :url "activity-log"
64 | :component page
65 | :init-fx [::init]})
66 |
--------------------------------------------------------------------------------
/test/clj/ventas/utils_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.utils-test
2 | (:require
3 | [clojure.core.async :as core.async :refer [! go]]
4 | [clojure.spec.alpha :as spec]
5 | [clojure.test :refer [deftest is testing]]
6 | [ventas.utils :as sut]))
7 |
8 | (deftest chan?
9 | (is (sut/chan? (core.async/chan))))
10 |
11 | (deftest swallow
12 | (is (= nil (sut/swallow (throw (Exception. "Test"))))))
13 |
14 | (deftest spec-exists?
15 | (spec/def ::example integer?)
16 | (is (not (sut/spec-exists? ::does-not-exist)))
17 | (is (sut/spec-exists? ::example)))
18 |
19 | (deftest dequalify-keywords
20 | (is (= {:a :b
21 | :c :d}
22 | (sut/dequalify-keywords {:ns/a :b
23 | :ns/c :d}))))
24 |
25 | (deftest qualify-keyword
26 | (is (= :some-other.ns/test
27 | (sut/qualify-keyword :test :some-other.ns))))
28 |
29 | (deftest qualify-map-keywords
30 | (is (= {::a :b
31 | ::c :d}
32 | (sut/qualify-map-keywords {:a :b
33 | :c :d}
34 | (namespace ::kw)))))
35 |
36 | (deftest check
37 | (spec/def ::test-spec.a integer?)
38 | (spec/def ::test-spec
39 | (spec/keys :req [::test-spec.a]))
40 | (is (sut/check ::test-spec {::test-spec.a 5}))
41 | (is (thrown? Exception
42 | (sut/check ::test-spec {::test-spec.a "test"}))))
43 |
44 | (deftest update-if-exists
45 | (is (= {:a 1}
46 | (sut/update-if-exists {:a 0} :a inc)))
47 | (is (= {:a 0}
48 | (sut/update-if-exists {:a 0} :b inc))))
49 |
50 | (deftest has-duplicates?
51 | (is (sut/has-duplicates? [:a :b :c :b]))
52 | (is (not (sut/has-duplicates? [:a :b :c]))))
53 |
54 | (deftest ns-kw
55 | (is (thrown? Exception (sut/ns-kw "kw"))))
56 |
57 | (deftest bigdec?
58 | (is (sut/bigdec? 5M)))
59 |
60 | (deftest ->number
61 | (is (= 5
62 | (sut/->number "5"))))
63 |
64 | (deftest batch
65 | (let [in (core.async/chan (core.async/sliding-buffer 10))
66 | out (core.async/chan)
67 | _ (sut/batch in out 50 5)
68 | ch (go
69 | (dotimes [_ 7]
70 | (>! in true))
71 | (is (= (repeat 5 true) (> (all)
60 | (filter (fn [[_ v]]
61 | (if (= type :plugin)
62 | (not (:type v))
63 | (= (:type v) type))))
64 | (into {})))
65 |
66 | (defn check! [kw]
67 | {:pre [(keyword? kw)]}
68 | (if-not (find kw)
69 | (throw+ {:type ::plugin-not-found
70 | :keyword kw})
71 | true))
72 |
73 | (defn fixtures [kw]
74 | {:pre [(keyword? kw)]}
75 | (check! kw)
76 | (when-let [fixtures-fn (:fixtures (find kw))]
77 | (fixtures-fn)))
78 |
79 | (defn handle-request
80 | "Processes HTTP requests directed to the plugins"
81 | [kw path]
82 | {:pre [(check! kw)]}
83 | (let [plugin (find kw)]
84 | ((:http-handler plugin) path)))
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/configuration/general.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.configuration.general
2 | (:require
3 | [re-frame.core :as rf]
4 | [reagent.ratom :refer [atom]]
5 | [ventas.components.base :as base]
6 | [ventas.components.form :as form]
7 | [ventas.components.notificator :as notificator]
8 | [ventas.server.api.admin :as api.admin]
9 | [ventas.themes.admin.common :refer [entity->option]]
10 | [ventas.i18n :refer [i18n]]
11 | [ventas.themes.admin.skeleton :as admin.skeleton]
12 | [ventas.routes :as routes]
13 | [ventas.utils.ui :as utils.ui])
14 | (:require-macros
15 | [ventas.utils :refer [ns-kw]]))
16 |
17 | (def state-key ::state)
18 |
19 | (def form-path [state-key])
20 |
21 | (rf/reg-event-fx
22 | ::submit
23 | (fn [{:keys [db]} _]
24 | {:dispatch [::api.admin/admin.general-config.set
25 | {:params (form/get-data db form-path)
26 | :success [::notificator/notify-saved]}]}))
27 |
28 | (defn- field [{:keys [key] :as args}]
29 | [form/field (merge args
30 | {:db-path [state-key]
31 | :label (i18n (ns-kw key))})])
32 |
33 | (rf/reg-sub
34 | ::culture-options
35 | (fn [db]
36 | (map entity->option (get-in db [state-key :cultures]))))
37 |
38 | (defn- content []
39 | [form/form [state-key]
40 | [base/segment {:color "orange"
41 | :title (i18n ::page)}
42 | [base/form {:on-submit (utils.ui/with-handler #(rf/dispatch [::submit]))}
43 |
44 | [field {:key :general-config/culture
45 | :type :combobox
46 | :options @(rf/subscribe [::culture-options])}]
47 |
48 | [base/divider {:hidden true}]
49 |
50 | [base/form-button
51 | {:type "submit"}
52 | (i18n ::submit)]]]])
53 |
54 | (defn- page []
55 | [admin.skeleton/skeleton
56 | [:div.admin__default-content.admin-general-configuration__page
57 | [content]]])
58 |
59 | (rf/reg-event-fx
60 | ::init
61 | (fn [_ _]
62 | {:dispatch-n [[::api.admin/admin.general-config.get
63 | {:success [::form/populate [state-key]]}]
64 | [::api.admin/admin.entities.list
65 | {:params {:type :i18n.culture}
66 | :success [:db [state-key :cultures]]}]]}))
67 |
68 | (routes/define-route!
69 | :admin.configuration.general
70 | {:name ::page
71 | :url "general"
72 | :component page
73 | :init-fx [::init]})
74 |
--------------------------------------------------------------------------------
/src/clj/ventas/entities/brand.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.brand
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [ventas.database.entity :as entity]
5 | [ventas.search :as search]
6 | [ventas.database.generators :as generators]
7 | [ventas.entities.i18n :as entities.i18n]
8 | [ventas.utils :refer [update-if-exists]]
9 | [ventas.utils.slugs :as utils.slugs]
10 | [ventas.search.schema :as search.schema]))
11 |
12 | (spec/def :brand/name ::entities.i18n/ref)
13 |
14 | (spec/def :brand/description ::entities.i18n/ref)
15 |
16 | (spec/def :brand/keyword ::generators/keyword)
17 |
18 | (spec/def :brand/logo
19 | (spec/with-gen ::entity/ref #(entity/ref-generator :file)))
20 |
21 | (spec/def :schema.type/brand
22 | (spec/keys :req [:brand/name
23 | :brand/description]
24 | :opt [:brand/logo
25 | :brand/keyword]))
26 |
27 | (entity/register-type!
28 | :brand
29 | {:migrations
30 | [[:base [{:db/ident :brand/name
31 | :db/isComponent true
32 | :db/valueType :db.type/ref
33 | :db/cardinality :db.cardinality/one
34 | :ventas/refEntityType :i18n}
35 |
36 | {:db/ident :brand/keyword
37 | :db/valueType :db.type/keyword
38 | :db/unique :db.unique/identity
39 | :db/cardinality :db.cardinality/one}
40 |
41 | {:db/ident :brand/description
42 | :db/isComponent true
43 | :db/valueType :db.type/ref
44 | :db/cardinality :db.cardinality/one
45 | :ventas/refEntityType :i18n}
46 |
47 | {:db/ident :brand/logo
48 | :db/valueType :db.type/ref
49 | :db/cardinality :db.cardinality/one}]]]
50 |
51 | :autoresolve? true
52 |
53 | :filter-create
54 | (fn [this]
55 | (utils.slugs/add-slug-to-entity this :brand/name))
56 |
57 | :filter-update
58 | (fn [_ attrs]
59 | (utils.slugs/add-slug-to-entity attrs :brand/name))
60 |
61 | :dependencies
62 | #{:file :i18n}})
63 |
64 | (search/configure-type!
65 | :brand
66 | {:migrations
67 | [[:base {:properties
68 | (merge #:brand{:keyword {:type "keyword"}
69 | :logo {:type "long"}}
70 | (entities.i18n/es-migration
71 | {:brand/description {:type "text"}
72 | :brand/name (search.schema/autocomplete-type)}
73 | [:en_US :es_ES]))}]]})
--------------------------------------------------------------------------------
/test/clj/ventas/server/api/description_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.server.api.description-test
2 | (:require
3 | [clojure.test :refer [deftest is testing use-fixtures]]
4 | [spec-tools.data-spec :as data-spec]
5 | [ventas.database.entity :as entity]
6 | [ventas.server.api :as api]
7 | [ventas.server.pagination :as pagination]
8 | [ventas.server.ws :as server.ws]
9 | [ventas.test-tools :as test-tools]
10 | [ventas.server.api.description :as sut]
11 | [ventas.utils :as utils]))
12 |
13 | (defn example-user []
14 | {:schema/type :schema.type/user
15 | :user/first-name "Test user"
16 | :user/roles #{:user.role/administrator}
17 | :user/email (str (gensym "test-user") "@test.com")})
18 |
19 | (declare user)
20 |
21 | (use-fixtures :once #(test-tools/with-test-context
22 | (with-redefs [user (entity/create* (example-user))]
23 | (%))))
24 |
25 | (deftest describe
26 | (with-redefs [api/available-requests (atom {:test {:binary? false
27 | :spec {:some integer?
28 | :thing string?
29 | :stuff [integer?]}
30 | :doc "Documentation!"
31 | :middlewares [pagination/paginate]}})]
32 | (is (= {:test {:doc "Documentation!"
33 | :spec {:keys {:some {:type :number}
34 | :stuff {:items {:type :number
35 | :title :api$test/stuff} :type :vector}
36 | :thing {:type :string}}
37 | :required [:some :thing :stuff]
38 | :title :api/test
39 | :type :map}}}
40 | (-> (server.ws/call-handler-with-user ::sut/api.describe {} user)
41 | :data)))))
42 |
43 | (deftest generate-params
44 | (dotimes [n 10]
45 | (is (utils/check
46 | (data-spec/spec :api/products.aggregations (get-in @api/available-requests [::api/products.aggregations :spec]))
47 | (:data
48 | (server.ws/call-handler-with-user ::sut/api.generate-params
49 | {:request :products.aggregations}
50 | user))))))
51 |
--------------------------------------------------------------------------------
/src/cljs/ventas/components/search_box.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.search-box
2 | (:require
3 | [ventas.components.base :as base]
4 | [ventas.i18n :refer [i18n]]
5 | [re-frame.core :as rf]
6 | [reagent.core :as r]
7 | [ventas.server.api :as backend]
8 | [ventas.components.image :as image])
9 | (:require-macros
10 | [ventas.utils :refer [ns-kw]]))
11 |
12 | (def state-key ::state)
13 |
14 | (rf/reg-sub
15 | ::items
16 | (fn [db [_ id]]
17 | (get-in db [state-key id :search])))
18 |
19 | (rf/reg-sub
20 | ::query
21 | (fn [db [_ id]]
22 | (get-in db [state-key id :search-query])))
23 |
24 | (rf/reg-event-db
25 | ::query.set
26 | (fn [db [_ id query]]
27 | (assoc-in db [state-key id :search-query] query)))
28 |
29 | (rf/reg-event-db
30 | ::items.set
31 | (fn [db [_ id items]]
32 | (if (empty? items)
33 | (update-in db [state-key id] #(dissoc % :search))
34 | (assoc-in db [state-key id :search] items))))
35 |
36 | (rf/reg-event-fx
37 | ::search
38 | (fn [_ [_ id search-event query]]
39 | {:dispatch-n
40 | (if (empty? query)
41 | [[::items.set id nil]]
42 | [[::query.set id query]
43 | (if search-event
44 | (conj search-event query)
45 | [::backend/search {:params {:search query}
46 | :success [::items.set id]}])])}))
47 |
48 | (defn- search-result-view [on-result-click {:keys [name image type] :as item}]
49 | [:div.search-result
50 | (when on-result-click
51 | {:on-click (partial on-result-click item)})
52 | (when image
53 | [:img {:src (image/get-url (:id image) :header-search)}])
54 | [:div.search-result__right
55 | [:p.search-result__name name]
56 | (when type
57 | [:p.search-result__type (i18n (keyword "schema.type" type))])]])
58 |
59 | (defn ->options [on-result-click coll]
60 | (map (fn [item]
61 | {:value (:id item)
62 | :text (:name item)
63 | :key (:id item)
64 | :content (r/as-element [search-result-view on-result-click item])})
65 | coll))
66 |
67 | (defn search-box [{:keys [id on-result-click search-event] :as props}]
68 | [base/dropdown
69 | (merge
70 | {:placeholder (i18n ::search)
71 | :class "search-box"
72 | :selection true
73 | :icon "search"
74 | :search (fn [options _] options)
75 | :options (->options on-result-click @(rf/subscribe [::items id]))
76 | :on-search-change #(rf/dispatch [::search id search-event (-> % .-target .-value)])}
77 | props)])
--------------------------------------------------------------------------------
/src/scss/ventas/pages/admin/customization/_customize.scss:
--------------------------------------------------------------------------------
1 |
2 | .root--customize {
3 | .admin__sidebar {
4 | display: none;
5 | }
6 | .cookies {
7 | display: none;
8 | }
9 | }
10 |
11 | .root {
12 | position: relative;
13 | }
14 |
15 | .admin-customize__page {
16 | display: flex;
17 | padding: 0px;
18 | color: #2c3e50;
19 |
20 | .admin-customize__content {
21 | background-color: white;
22 | width: 300px;
23 | padding: 15px;
24 | box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.52);
25 | margin-right: 3px;
26 | position: fixed;
27 | overflow: visible;
28 | height: 100vh;
29 | z-index: 1;
30 | }
31 |
32 | #main {
33 | width: 100%;
34 | margin-left: 303px;
35 | }
36 | }
37 |
38 | .customize-field {
39 | display: flex;
40 | flex-direction: row;
41 | align-items: center;
42 | flex-wrap: wrap;
43 | border-bottom: solid 1px rgba(0, 0, 0, 0.08);
44 |
45 | .ui.header {
46 | font-weight: bold;
47 | margin: 0;
48 | flex-grow: 1;
49 | padding: 13px 0px;
50 | }
51 | }
52 |
53 | .customize-field__input {
54 | &--text {
55 | width: 100%;
56 | }
57 | &--image {
58 | cursor: pointer;
59 |
60 | input {
61 | display: none;
62 | }
63 | }
64 | }
65 |
66 | .customize-field__color {
67 | width: 16px;
68 | height: 16px;
69 | border-radius: 50%;
70 | border: solid 1px rgba(0, 0, 0, 0.5);
71 | }
72 |
73 | .customize-field__colorpicker {
74 | position: relative;
75 | top: -16px;
76 | left: 25px;
77 | z-index: 9999;
78 |
79 | &::before {
80 | display: block;
81 | content: " ";
82 | position: absolute;
83 | top: 15px;
84 | left: -9px;
85 | width: 0;
86 | height: 0;
87 | border-style: solid;
88 | border-width: 7px 8px 7px 0;
89 | border-color: transparent #6b6b6b transparent transparent;
90 | }
91 | }
92 |
93 | .customize-field__radio {
94 | display: inline-block;
95 | border: solid 1px #989898;
96 | padding: 2px 4px;
97 | margin-left: 2px;
98 |
99 | &--active {
100 | background-color: #989898;
101 | i.icon {
102 | color: white;
103 | }
104 | }
105 |
106 | i.icon {
107 | margin: 0px;
108 | }
109 | }
--------------------------------------------------------------------------------
/src/cljs/ventas/components/slider.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.slider
2 | (:require
3 | [cljs.core.async :refer [! alts! chan go timeout]]
4 | [re-frame.core :as rf]))
5 |
6 | (def transition-duration-ms 250)
7 |
8 | (defn- get-dimension [v]
9 | (if (= v ::viewport)
10 | (-> js/window .-innerWidth)
11 | v))
12 |
13 | (rf/reg-sub
14 | ::offset
15 | (fn [db [_ state-path]]
16 | (let [{:keys [current-index slides orientation]} (get-in db state-path)]
17 | (* -1 (reduce (fn [sum idx]
18 | (let [slide (get slides idx)]
19 | (+ sum (if (= orientation :vertical)
20 | (get-dimension (:height slide))
21 | (get-dimension (:width slide))))))
22 | 0
23 | (range current-index))))))
24 |
25 | (rf/reg-sub
26 | ::slides
27 | (fn [db [_ state-path]]
28 | (let [{:keys [render-index slides visible-slides]} (get-in db state-path)]
29 | (when (and render-index slides)
30 | (->> (cycle slides)
31 | (drop render-index)
32 | (take (if (<= visible-slides (count slides))
33 | (+ 2 (count slides))
34 | (count slides))))))))
35 |
36 | (def update-stage (atom nil))
37 |
38 | (defn- update-current-index [db state-path increment]
39 | (let [{:keys [visible-slides slides]} (get-in db state-path)]
40 | (when (<= visible-slides (count slides))
41 | (if (= :started @update-stage)
42 | db
43 | (do
44 | (reset! update-stage :started)
45 | (go ( state
50 | (update :render-index #(mod (+ % increment) (count (:slides state))))
51 | (update :current-index #(- % increment))))])
52 | (reset! update-stage :finished))
53 | (update-in db state-path (fn [state]
54 | (-> state
55 | (update :current-index #(+ % increment))))))))))
56 |
57 | (rf/reg-event-db
58 | ::next
59 | (fn [db [_ state-path]]
60 | (or (update-current-index db state-path 1)
61 | db)))
62 |
63 | (rf/reg-event-db
64 | ::previous
65 | (fn [db [_ state-path]]
66 | (or (update-current-index db state-path -1)
67 | db)))
68 |
--------------------------------------------------------------------------------
/src/clj/ventas/storage.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.storage
2 | (:require
3 | [clojure.java.io :as io]
4 | [mount.core :as mount :refer [defstate]]
5 | [ventas.storage.protocol :as protocol]
6 | [clojure.string :as str])
7 | (:import [java.io File]))
8 |
9 | (def
10 | ^{:dynamic true
11 | :doc "Should contain a StorageBackend implementation"}
12 | storage-backend)
13 |
14 | (defn- key->path [base-path key]
15 | (str base-path (when key (str "/" key))))
16 |
17 | (defn- key->file [base-path key]
18 | (io/file (key->path base-path key)))
19 |
20 | (defrecord LocalStorageBackend [base-path]
21 | protocol/StorageBackend
22 | (get-object [_ key]
23 | (let [file (key->file base-path key)]
24 | (when (.exists file)
25 | (io/input-stream file))))
26 | (get-public-url [_ key]
27 | (str "/" (key->path base-path key)))
28 | (list-objects [this]
29 | (protocol/list-objects this ""))
30 | (list-objects [_ prefix]
31 | (->> (file-seq (io/file (key->path base-path prefix)))
32 | (map (fn [file]
33 | (-> (str file)
34 | (str/replace (str "src" File/separator) "")
35 | (str/replace File/separator "/"))))))
36 | (remove-object [_ key]
37 | (io/delete-file (key->file base-path key) true))
38 | (stat-object [_ key]
39 | (let [file (key->file base-path key)]
40 | (when (.exists file)
41 | {:length (.length file)
42 | :last-modified (-> (.lastModified file)
43 | (/ 1000) (long) (* 1000)
44 | (java.util.Date.))})))
45 | (put-object [_ key file]
46 | (let [target-file (key->file base-path key)
47 | file (if (string? file) (io/file file) file)]
48 | (io/make-parents target-file)
49 | (when-not (.exists target-file)
50 | (io/copy file target-file)))))
51 |
52 | (defstate storage-backend
53 | :start
54 | (let [base-path "storage"]
55 | (.mkdir (io/file base-path))
56 | (->LocalStorageBackend base-path)))
57 |
58 | (defn get-object [key]
59 | (protocol/get-object storage-backend key))
60 |
61 | (defn remove-object [key]
62 | (protocol/remove-object storage-backend key))
63 |
64 | (defn get-public-url [key]
65 | (protocol/get-public-url storage-backend key))
66 |
67 | (defn stat-object [key]
68 | (protocol/stat-object storage-backend key))
69 |
70 | (defn put-object [key file]
71 | (protocol/put-object storage-backend key file))
72 |
73 | (defn list-objects [& [prefix]]
74 | (if prefix
75 | (protocol/list-objects storage-backend prefix)
76 | (protocol/list-objects storage-backend)))
--------------------------------------------------------------------------------
/src/cljs/ventas/utils.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.utils
2 | "Random utilities"
3 | (:require
4 | [cljs.spec.alpha :as spec]
5 | [expound.alpha :as expound]
6 | [ventas.utils.logging :refer [debug error info trace warn]]
7 | [ventas.utils.logging :as log])
8 | (:require-macros
9 | [ventas.utils]))
10 |
11 | (defn value-handler [callback]
12 | (fn [e]
13 | (callback (-> e .-target .-value))))
14 |
15 | (defn child? [element target]
16 | (when-let [parent (.-parentNode element)]
17 | (if (= target parent)
18 | true
19 | (child? parent target))))
20 |
21 | (defn interpose-fn
22 | "Returns a lazy seq of the elements of coll separated by sep.
23 | Returns a stateful transducer when no collection is provided."
24 | {:added "1.0"
25 | :static true}
26 | ([sep]
27 | (fn [rf]
28 | (let [started (volatile! false)]
29 | (fn
30 | ([] (rf))
31 | ([result] (rf result))
32 | ([result input]
33 | (if @started
34 | (let [sepr (rf result sep)]
35 | (if (reduced? sepr)
36 | sepr
37 | (rf sepr input)))
38 | (do
39 | (vreset! started true)
40 | (rf result input))))))))
41 | ([sep coll]
42 | (drop 1 (interleave (repeatedly sep) coll))))
43 |
44 | (defn check [spec x]
45 | (when-not (spec/valid? spec x)
46 | (log/error (with-out-str (expound/expound spec x))))
47 | true)
48 |
49 | (defn debounce
50 | "Returns a function that will call f only after threshold has passed without new calls
51 | to the function. Calls prep-fn on the args in a sync way, which can be used for things like
52 | calling .persist on the event object to be able to access the event attributes in f"
53 | ([threshold f] (debounce threshold f (constantly nil)))
54 | ([threshold f prep-fn]
55 | (let [t (atom nil)]
56 | (fn [& args]
57 | (when @t (js/clearTimeout @t))
58 | (apply prep-fn args)
59 | (reset! t (js/setTimeout #(do
60 | (reset! t nil)
61 | (apply f args))
62 | threshold))))))
63 |
64 | (defn parse-int [n]
65 | (js/parseInt n 10))
66 |
67 | (defn render-with-indexes [& coll]
68 | (map-indexed (fn [idx itm]
69 | (with-meta itm {:key idx}))
70 | coll))
71 |
72 | (defn cut-string-on-space [s max-chars]
73 | (when s
74 | (let [last-idx (.lastIndexOf (take max-chars s) \space)
75 | idx (if (pos? last-idx)
76 | last-idx
77 | max-chars)]
78 | (subs s 0 idx))))
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).
3 |
4 |
5 |
6 | ## 0.1.1 - 2019-04-18
7 |
8 | ### Fixed
9 |
10 | - shadow-cljs now uses leiningen for source-paths and dependencies management.
11 |
12 | ## 0.1.0 - 2019-04-18
13 | ### Changed
14 |
15 | - When changing a child entity, the parent entity will also be indexed.
16 | - The new way of specifying how entities are indexed is via `search/configure-type!`, which allows you to register ES migrations. If `search/configure-type!` is not called, that type won't be indexed.
17 | - The tx-report-queue loop has been removed and a new `ventas.tx-processor` has been created, which allows users to add callbacks, which will be called whenever a transaction is processed. This is used for processing the new `:after-transact` entity type property, and for indexing.
18 | - All side-effectful entity lifecycle functions (before-..., after-...) and filter-query have been removed. `:after-transact` has been added, which should be more than enough to cover the usecases for the removed lifecycle functions, and it is called recursively (the previous lifecycle functions would just be fired for direct calls to `entity/create` or `entity/update`, so child entities were being ignored).
19 | - `ventas.entities.configuration` is being deprecated and the email configuration is done with its own entity type.
20 | - Thumbnailator is used now for image resizing, instead of fivetonine/collage.
21 | - `product/images` is using the new `:file.list` entity type now, and a new form input has been added to edit such kind of entities.
22 |
23 | ### Removed
24 |
25 | - All seeding-related lifecycle functions have been removed, and there is no replacement. The same functionality can be achieved using standard lifecycle functions (plus the new `:after-transact` , described above)
26 |
27 | ### Fixed
28 |
29 | - clojure.tools.nrepl.middleware not found error, caused by wrongly specifying the shadow-cljs middleware
30 |
31 | ### Added
32 |
33 | - This document
34 | - Allow using profiles in the configuration EDN file (and get the profile from the VENTAS_PROFILE environment variable)
35 | - Removing a document from Datomic now removes it from Elasticsearch too.
36 | - `db/etouch` and `db/map-etouch`, with the intention of deprecating `db/touch-eid`, which is wasteful and goes against the Datomic model.
37 | - An storage backend abstraction, which allows you to use something different than the local filesystem (S3, for example)
38 | - Websocket ping/pong requests/responses, to keep the connection alive
39 | - Both `entity/create` and `entity/update` return the tx as meta now.
40 |
--------------------------------------------------------------------------------
/src/cljs/ventas/local_storage.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.local-storage
2 | "Slightly modified version of https://github.com/akiroz/re-frame-storage"
3 | (:require
4 | [alandipert.storage-atom :refer [local-storage]]
5 | [cljs.spec.alpha :as spec]
6 | [re-frame.core :refer [->interceptor reg-cofx reg-fx]]))
7 |
8 | (spec/def ::cljs-data
9 | (spec/or :nil nil?
10 | :boolean boolean?
11 | :number number?
12 | :string string?
13 | :keyword keyword?
14 | :symbol symbol?
15 | :uuid uuid?
16 | :date (partial instance? js/Date)
17 | :list (spec/coll-of ::cljs-data :kind list?)
18 | :vector (spec/coll-of ::cljs-data :kind vector?)
19 | :set (spec/coll-of ::cljs-data :kind set?)
20 | :map (spec/map-of ::cljs-data ::cljs-data)))
21 |
22 | (def storage-atoms (atom {}))
23 |
24 | (defn register-store [store-key]
25 | (when-not (@storage-atoms store-key)
26 | (swap! storage-atoms assoc store-key
27 | (local-storage (atom nil) store-key))))
28 |
29 | (spec/fdef register-store
30 | :args (spec/cat :store-key keyword?))
31 |
32 | (defn ->store [store-key data]
33 | (reset! (@storage-atoms store-key) data))
34 |
35 | (spec/fdef ->store
36 | :args (spec/cat :store-key keyword?
37 | :data ::cljs-data))
38 |
39 | (defn <-store [store-key]
40 | @(@storage-atoms store-key))
41 |
42 | (spec/fdef <-store
43 | :args (spec/cat :store-key keyword?)
44 | :ret ::cljs-data)
45 |
46 | (defn reg-co-fx! [store-key {:keys [fx cofx]}]
47 | (register-store store-key)
48 | (when fx
49 | (reg-fx
50 | fx
51 | (fn [data]
52 | (->store store-key data))))
53 | (when cofx
54 | (reg-cofx
55 | cofx
56 | (fn [coeffects _]
57 | (assoc coeffects cofx (<-store store-key))))))
58 |
59 | (spec/def ::fx keyword?)
60 | (spec/def ::cofx keyword?)
61 | (spec/fdef reg-co-fx!
62 | :args (spec/cat :store-key keyword?
63 | :handlers (spec/keys :req [(or ::fx ::cofx)])))
64 |
65 | (defn persist-db [store-key db-key]
66 | (register-store store-key)
67 | (->interceptor
68 | :id (keyword (str db-key "->" store-key))
69 | :before (fn [context]
70 | (assoc-in context [:coeffects :db db-key]
71 | (<-store store-key)))
72 | :after (fn [context]
73 | (when-let [value (get-in context [:effects :db db-key])]
74 | (->store store-key value))
75 | context)))
76 |
77 | (spec/fdef persist-db
78 | :args (spec/cat :store-key keyword?
79 | :db-key keyword?))
80 |
--------------------------------------------------------------------------------
/test/clj/ventas/entities/category_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.category-test
2 | (:require
3 | [clojure.test :refer [deftest is testing use-fixtures]]
4 | [ventas.database :as db]
5 | [ventas.database.entity :as entity]
6 | [ventas.database.seed :as seed]
7 | [ventas.entities.category :as sut]
8 | [ventas.entities.i18n :as entities.i18n]
9 | [ventas.test-tools :as test-tools]))
10 |
11 | (def example-category
12 | {:category/name (entities.i18n/->entity {:en_US "Example category"})
13 | :category/keyword :example-category
14 | :category/image {:schema/type :schema.type/file
15 | :file/extension "jpg"}
16 | :category/parent {:category/name (entities.i18n/->entity {:en_US "Example parent category"})
17 | :category/keyword :example-category-parent
18 | :category/image {:schema/type :schema.type/file
19 | :file/extension "jpg"}
20 | :category/parent {:category/name (entities.i18n/->entity {:en_US "Example root category"})
21 | :category/keyword :example-category-root
22 | :schema/type :schema.type/category}
23 | :schema/type :schema.type/category}
24 | :schema/type :schema.type/category})
25 |
26 | (declare category)
27 |
28 | (use-fixtures :once #(test-tools/with-test-context
29 | (seed/seed :minimal? true)
30 | (with-redefs [category (entity/create* example-category)]
31 | (%))))
32 |
33 | (deftest serialization
34 | (is (= "jpg" (get-in (entity/serialize category) [:image :extension])))
35 | (is (not (:category/image (entity/deserialize :category (entity/serialize category))))))
36 |
37 | (deftest get-image
38 | (is (= "jpg" (:extension (sut/get-image category)))))
39 |
40 | (deftest get-parents
41 | (let [pulled-category (db/pull '[{:category/parent [*]} *] (:db/id category))]
42 | (is (= #{(get-in pulled-category [:db/id])
43 | (get-in pulled-category [:category/parent :db/id])
44 | (get-in pulled-category [:category/parent :category/parent :db/id])}
45 | (sut/get-parents category)))))
46 |
47 | (deftest get-parent-slug
48 | (is (= "example-parent-category-example-category"
49 | (-> (sut/get-parent-slug (:db/id category))
50 | :i18n/translations
51 | first
52 | :i18n.translation/value))))
53 |
54 | (deftest add-slug-to-category
55 | (let [slug (:ventas/slug (#'sut/add-slug-to-category category))]
56 | (is slug)
57 | (is (= "example-parent-category-example-category"
58 | (entity/find-serialize slug {:culture [:i18n.culture/keyword :en_US]})))))
59 |
--------------------------------------------------------------------------------
/src/clj/ventas/entities/image_size.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.image-size
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [clojure.test.check.generators :as gen]
5 | [ventas.database :as db]
6 | [ventas.database.entity :as entity]
7 | [ventas.database.generators :as generators]
8 | [ventas.utils :as utils]
9 | [ventas.utils.files :refer [basename]]))
10 |
11 | (spec/def :image-size/keyword ::generators/keyword)
12 |
13 | (spec/def :image-size/width
14 | (spec/with-gen number?
15 | #(gen/choose 100 400)))
16 |
17 | (spec/def :image-size/height
18 | (spec/with-gen number?
19 | #(gen/choose 100 400)))
20 |
21 | (def algorithms
22 | #{:image-size.algorithm/resize-only-if-over-maximum
23 | :image-size.algorithm/always-resize
24 | :image-size.algorithm/crop-and-resize})
25 |
26 | (spec/def :image-size/algorithm
27 | (spec/with-gen
28 | (spec/or :pull-eid ::db/pull-eid
29 | :algorithm algorithms)
30 | #(gen/elements algorithms)))
31 |
32 | (spec/def :image-size/quality
33 | (spec/with-gen number?
34 | #(gen/double*
35 | {:infinite? false
36 | :NaN? false
37 | :min 0.0
38 | :max 1.0})))
39 |
40 | (spec/def :schema.type/image-size
41 | (spec/keys :req [:image-size/keyword
42 | :image-size/width
43 | :image-size/height
44 | :image-size/algorithm]
45 | :opt [:image-size/quality]))
46 |
47 | (entity/register-type!
48 | :image-size
49 | {:migrations
50 | [[:base (utils/into-n
51 | [{:db/ident :image-size/keyword
52 | :db/valueType :db.type/keyword
53 | :db/unique :db.unique/identity
54 | :db/cardinality :db.cardinality/one}
55 |
56 | {:db/ident :image-size/width
57 | :db/valueType :db.type/long
58 | :db/cardinality :db.cardinality/one}
59 |
60 | {:db/ident :image-size/height
61 | :db/valueType :db.type/long
62 | :db/cardinality :db.cardinality/one}
63 |
64 | {:db/ident :image-size/algorithm
65 | :db/valueType :db.type/ref
66 | :db/cardinality :db.cardinality/one
67 | :ventas/refEntityType :enum}
68 |
69 | {:db/ident :image-size/quality
70 | :db/valueType :db.type/float
71 | :db/cardinality :db.cardinality/one}
72 |
73 | {:db/ident :image-size/entities
74 | :db/valueType :db.type/ref
75 | :db/cardinality :db.cardinality/many
76 | :ventas/refEntityType :enum}]
77 |
78 | (map #(hash-map :db/ident %) algorithms))]
79 | [:deprecate-entities [{:db/id :image-size/entities
80 | :schema/deprecated true
81 | :schema/see-instead :file/image-sizes}]]]})
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/products/discounts.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.products.discounts
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.components.base :as base]
5 | [ventas.components.table :as table]
6 | [ventas.events :as events]
7 | [ventas.server.api.admin :as api.admin]
8 | [ventas.i18n :refer [i18n]]
9 | [ventas.themes.admin.skeleton :as admin.skeleton]
10 | [ventas.routes :as routes]))
11 |
12 | (def state-key ::state)
13 |
14 | (defn- action-column [{:keys [id]}]
15 | [:div
16 | [base/button {:icon true :on-click #(rf/dispatch [::events/admin.entities.remove [state-key :discounts] id])}
17 | [base/icon {:name "remove"}]]])
18 |
19 | (defn- footer []
20 | [base/button
21 | {:on-click #(routes/go-to :admin.products.discounts.edit :id 0)}
22 | (i18n ::create)])
23 |
24 | (rf/reg-event-fx
25 | ::fetch
26 | (fn [{:keys [db]} [_ state-path]]
27 | (let [{:keys [page items-per-page sort-direction sort-column]} (table/get-state db state-path)]
28 | {:dispatch [::api.admin/admin.entities.list
29 | {:success ::fetch.next
30 | :params {:type :discount
31 | :pagination {:page page
32 | :items-per-page items-per-page}
33 | :sorting {:direction sort-direction
34 | :field sort-column}}}]})))
35 |
36 | (rf/reg-event-db
37 | ::fetch.next
38 | (fn [db [_ {:keys [items total]}]]
39 | (-> db
40 | (assoc-in [state-key :table :rows] items)
41 | (assoc-in [state-key :table :total] total))))
42 |
43 | (defn- content []
44 | [:div.admin-discounts__table
45 | [table/table [state-key :table]]])
46 |
47 | (defn page []
48 | [admin.skeleton/skeleton
49 | [:div.admin__default-content.admin-discounts__page
50 | [content]]])
51 |
52 | (rf/reg-event-fx
53 | ::init
54 | (fn [_ _]
55 | {:dispatch [::table/init [state-key :table]
56 | {:fetch-fx [::fetch]
57 | :columns [{:id :name
58 | :label (i18n ::name)
59 | :component (partial table/link-column :admin.products.discounts.edit :id :name)}
60 | {:id :code
61 | :label (i18n ::code)}
62 | {:id :amount
63 | :label (i18n ::amount)
64 | :component (partial table/amount-column :amount)}
65 | {:id :actions
66 | :label (i18n ::actions)
67 | :component action-column}]
68 | :footer footer}]}))
69 |
70 | (routes/define-route!
71 | :admin.products.discounts
72 | {:name ::page
73 | :url "discounts"
74 | :component page
75 | :init-fx [::init]})
76 |
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/products.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.products
2 | (:require
3 | [day8.re-frame.forward-events-fx]
4 | [re-frame.core :as rf]
5 | [ventas.components.base :as base]
6 | [ventas.components.table :as table]
7 | [ventas.events :as events]
8 | [ventas.server.api :as backend]
9 | [ventas.i18n :refer [i18n]]
10 | [ventas.themes.admin.skeleton :as admin.skeleton]
11 | [ventas.routes :as routes]))
12 |
13 | (def state-key ::state)
14 |
15 | (defn- action-column [{:keys [id]}]
16 | [:div
17 | [base/button {:icon true
18 | :on-click #(rf/dispatch [::events/admin.entities.remove [state-key :table :rows] id])}
19 | [base/icon {:name "remove"}]]])
20 |
21 | (defn- footer []
22 | [base/button
23 | {:on-click #(routes/go-to :admin.products.edit :id 0)}
24 | (i18n ::create-product)])
25 |
26 | (rf/reg-event-fx
27 | ::fetch
28 | (fn [{:keys [db]} [_ state-path]]
29 | (let [{:keys [page items-per-page sort-direction sort-column]} (table/get-state db state-path)]
30 | {:dispatch [::backend/products.list
31 | {:success ::fetch.next
32 | :params {:pagination {:page page
33 | :items-per-page items-per-page}
34 | :sorting {:direction sort-direction
35 | :field (if (= sort-column :price)
36 | [:price :value]
37 | sort-column)}}}]})))
38 |
39 | (rf/reg-event-db
40 | ::fetch.next
41 | (fn [db [_ {:keys [items total]}]]
42 | (-> db
43 | (assoc-in [state-key :table :rows] items)
44 | (assoc-in [state-key :table :total] total))))
45 |
46 | (defn- content []
47 | [:div.admin-products__table
48 | [table/table [state-key :table]]])
49 |
50 | (defn page []
51 | [admin.skeleton/skeleton
52 | [:div.admin__default-content.admin-products__page
53 | [content]]])
54 |
55 | (rf/reg-event-fx
56 | ::init
57 | (fn [_ _]
58 | {:dispatch [::table/init [state-key :table]
59 | {:fetch-fx [::fetch]
60 | :footer footer
61 | :columns [{:id :name
62 | :label (i18n ::name)
63 | :component (partial table/link-column :admin.products.edit :id :name)}
64 | {:id :price
65 | :label (i18n ::price)
66 | :component (partial table/amount-column :price)}
67 | {:id :actions
68 | :label (i18n ::actions)
69 | :component action-column}]}]}))
70 |
71 | (routes/define-route!
72 | :admin.products
73 | {:name ::page
74 | :url "products"
75 | :component page
76 | :init-fx [::init]})
77 |
--------------------------------------------------------------------------------
/src/clj/ventas/entities/currency.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.entities.currency
2 | (:require
3 | [clojure.spec.alpha :as spec]
4 | [ventas.database.entity :as entity]
5 | [ventas.database.generators :as generators]
6 | [ventas.entities.i18n :as entities.i18n]))
7 |
8 | (spec/def :currency/name ::entities.i18n/ref)
9 | (spec/def :currency/plural-name ::entities.i18n/ref)
10 | (spec/def :currency/keyword ::generators/keyword)
11 | (spec/def :currency/symbol ::generators/string)
12 | (spec/def :currency/culture
13 | (spec/with-gen ::entity/ref #(entity/ref-generator :i18n.culture)))
14 |
15 | (spec/def :schema.type/currency
16 | (spec/keys :req [:currency/name
17 | :currency/culture]
18 | :opt [:currency/keyword
19 | :currency/plural-name
20 | :currency/symbol]))
21 |
22 | (entity/register-type!
23 | :currency
24 | {:migrations
25 | [[:base [{:db/ident :currency/name
26 | :db/valueType :db.type/ref
27 | :db/cardinality :db.cardinality/one
28 | :db/isComponent true
29 | :ventas/refEntityType :i18n}
30 | {:db/ident :currency/plural-name
31 | :db/valueType :db.type/ref
32 | :db/cardinality :db.cardinality/one
33 | :db/isComponent true
34 | :ventas/refEntityType :i18n}
35 | {:db/ident :currency/symbol
36 | :db/valueType :db.type/string
37 | :db/cardinality :db.cardinality/one}
38 | {:db/ident :currency/keyword
39 | :db/valueType :db.type/keyword
40 | :db/cardinality :db.cardinality/one
41 | :db/unique :db.unique/identity}]]
42 | [:culture [{:db/ident :currency/culture
43 | :db/valueType :db.type/ref
44 | :ventas/refEntityType :i18n.culture
45 | :db/cardinality :db.cardinality/one}]]]
46 |
47 | :seed-number 0
48 |
49 | :autoresolve? true
50 |
51 | :dependencies
52 | #{:i18n}
53 |
54 | :fixtures
55 | ;; @TODO Import from CLDR?
56 | (fn []
57 | [{:currency/name (entities.i18n/->entity {:en_US "euro"
58 | :es_ES "euro"})
59 | :currency/plural-name (entities.i18n/->entity {:en_US "euros"
60 | :es_ES "euros"})
61 | :currency/keyword :eur
62 | :currency/symbol "€"
63 | :currency/culture [:i18n.culture/keyword :es_ES]}
64 | {:currency/name (entities.i18n/->entity {:en_US "dollar"
65 | :es_ES "dólar"})
66 | :currency/plural-name (entities.i18n/->entity {:en_US "dollars"
67 | :es_ES "dólares"})
68 | :currency/keyword :usd
69 | :currency/culture [:i18n.culture/keyword :en_US]
70 | :currency/symbol "$"}])})
71 |
--------------------------------------------------------------------------------
/src/cljs/ventas/themes/admin/configuration/image_sizes/edit.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.themes.admin.configuration.image-sizes.edit
2 | (:require
3 | [re-frame.core :as rf]
4 | [ventas.components.base :as base]
5 | [ventas.components.form :as form]
6 | [ventas.components.notificator :as notificator]
7 | [ventas.events :as events]
8 | [ventas.server.api.admin :as api.admin]
9 | [ventas.i18n :refer [i18n]]
10 | [ventas.themes.admin.skeleton :as admin.skeleton]
11 | [ventas.routes :as routes]
12 | [ventas.utils.logging :refer [debug error info trace warn]]
13 | [ventas.utils.ui :as utils.ui])
14 | (:require-macros
15 | [ventas.utils :refer [ns-kw]]))
16 |
17 | (def state-key ::state)
18 |
19 | (rf/reg-event-fx
20 | ::submit
21 | (fn [{:keys [db]}]
22 | {:dispatch [::api.admin/admin.entities.save
23 | {:params (get-in db [state-key :form])
24 | :success ::submit.next}]}))
25 |
26 | (rf/reg-event-fx
27 | ::submit.next
28 | (fn [_ _]
29 | {:dispatch [::notificator/notify-saved]
30 | :go-to [:admin.configuration.image-sizes]}))
31 |
32 | (rf/reg-event-fx
33 | ::init
34 | (fn [_ _]
35 | {:dispatch-n [(let [id (routes/ref-from-param :id)]
36 | (if-not (pos? id)
37 | [::form/populate [state-key] {:schema/type :schema.type/image-size}]
38 | [::api.admin/admin.entities.pull
39 | {:params {:id id}
40 | :success [::form/populate [state-key]]}]))
41 | [::events/enums.get :image-size.algorithm]]}))
42 |
43 | (defn- field [{:keys [key] :as args}]
44 | [form/field (merge args
45 | {:db-path [state-key]
46 | :label (i18n (ns-kw (if (sequential? key)
47 | (first key)
48 | key)))})])
49 |
50 | (defn content []
51 | [form/form [state-key]
52 | [base/form {:on-submit (utils.ui/with-handler #(rf/dispatch [::submit]))}
53 |
54 | [base/segment {:color "orange"
55 | :title "Image size"}
56 | [field {:key :image-size/keyword}]
57 | [field {:key :image-size/width}]
58 | [field {:key :image-size/height}]
59 | [field {:key [:image-size/algorithm :db/id]
60 | :type :combobox
61 | :options @(rf/subscribe [:db [:enums :image-size.algorithm]])}]]
62 |
63 | [base/divider {:hidden true}]
64 |
65 | [base/form-button {:type "submit"}
66 | (i18n ::submit)]]])
67 |
68 | (defn page []
69 | [admin.skeleton/skeleton
70 | [:div.admin__default-content.admin-configuration-image-sizes-edit__page
71 | [content]]])
72 |
73 | (routes/define-route!
74 | :admin.configuration.image-sizes.edit
75 | {:name ::page
76 | :url [:id "/edit"]
77 | :component page
78 | :init-fx [::init]})
79 |
--------------------------------------------------------------------------------
/test/clj/ventas/utils/images_test.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.utils.images-test
2 | (:require
3 | [clojure.test :refer [deftest is testing]]
4 | [ventas.utils.images :as sut]
5 | [ventas.test-tools :refer [with-test-image]]))
6 |
7 | (deftest transform-image
8 | (let [crop-args (atom nil)
9 | scale-args (atom nil)
10 | quality-args (atom nil)]
11 | (with-redefs [sut/source-region (fn [builder x y w h]
12 | (reset! crop-args [x y w h])
13 | (.sourceRegion builder x y w h))
14 | sut/scale-to (fn [builder scale]
15 | (reset! scale-args [scale])
16 | (.scale builder scale))
17 | sut/output-quality (fn [builder quality]
18 | (reset! quality-args [quality])
19 | (.outputQuality builder quality))]
20 | (with-test-image
21 | (fn [image]
22 | (sut/transform-image (str image)
23 | nil
24 | {:quality 0.76
25 | :resize {:width 50
26 | :height 50}
27 | :crop {:relation 1}})))
28 | (is (= @crop-args [2.5 0.0 95.0 95.0]))
29 | (is (= @scale-args [1/2]))
30 | (is (= @quality-args [0.76])))))
31 |
32 | (defn round2
33 | "Round a double to the given precision (number of significant digits)"
34 | [precision d]
35 | (let [factor (Math/pow 10 precision)]
36 | (/ (Math/round (* d factor)) factor)))
37 |
38 | (defn- crop-test [source-relation target-relation expectation]
39 | (let [source-region-args (atom nil)
40 | truncate-values (fn [numbers] (map (partial round2 1) numbers))]
41 | (with-redefs [sut/source-region (fn [_ & args] (reset! source-region-args args))]
42 | (#'sut/crop-image nil {:width 100 :height (/ 100 source-relation)} {:relation target-relation})
43 | (is (= (truncate-values expectation)
44 | (truncate-values @source-region-args))))))
45 |
46 | (deftest crop
47 | (testing "portrait to portait - lower"
48 | (crop-test 0.8 0.6 [12.5 0 75 125]))
49 | (testing "portrait to portrait - higher"
50 | (crop-test 0.8 0.9 [0 6.9 100 111.1]))
51 | (testing "portrait to landscape"
52 | (crop-test 0.8 1.2 [0 20.8 100 83.3]))
53 | (testing "portrait to square"
54 | (crop-test 0.8 1 [0 12.5 100 100]))
55 | (testing "landscape to portrait"
56 | (crop-test 1.2 0.8 [16.7 0 66.7 83.3]))
57 | (testing "landscape to landscape - lower"
58 | (crop-test 1.2 1.4 [0 6 100 71.4]))
59 | (testing "landscape to landscape - higher"
60 | (crop-test 1.2 1.1 [4.2 0 91.7 83.3]))
61 | (testing "landscape to square"
62 | (crop-test 1.2 1 [8.3 0 83.3 83.3])))
63 |
--------------------------------------------------------------------------------
/src/clj/ventas/database/seed.clj:
--------------------------------------------------------------------------------
1 | (ns ventas.database.seed
2 | (:require
3 | [clojure.set :as set]
4 | [slingshot.slingshot :refer [throw+]]
5 | [ventas.database.entity :as entity]
6 | [ventas.database.schema :as schema]
7 | [ventas.utils :as utils]
8 | [clojure.tools.logging :as log]))
9 |
10 | (defn seed-type
11 | "Seeds the database with n entities of a type"
12 | [type n]
13 | (doseq [fixture (entity/fixtures type)]
14 | (entity/create* fixture))
15 | (doseq [attributes (entity/generate (entity/kw->type type) n)]
16 | (entity/create* attributes)))
17 |
18 | (defn seed-type-with-deps
19 | [type n]
20 | (let [deps (entity/dependencies type)]
21 | (doseq [dep deps]
22 | (seed-type-with-deps dep 1))
23 | (log/info "Seeding type:" type)
24 | (seed-type type n)))
25 |
26 | (defn- get-sorted-types*
27 | [current remaining]
28 | (if (seq remaining)
29 | (let [new-types (->> remaining
30 | (utils/mapm (fn [type]
31 | [type (entity/dependencies type)]))
32 | (filter (fn [[type dependencies]]
33 | (or (empty? dependencies) (set/subset? dependencies (set current)))))
34 | (keys))]
35 | (get-sorted-types*
36 | (into current new-types)
37 | (set/difference remaining new-types)))
38 | current))
39 |
40 | (defn- detect-circular-dependencies! [types]
41 | (doseq [type types]
42 | (let [dependencies (entity/dependencies type)]
43 | (when (contains? dependencies type)
44 | (throw+ {:type ::self-dependency
45 | :entity-type type
46 | :message "An entity type cannot depend on itself"}))
47 | (doseq [dependency dependencies]
48 | (when-not (entity/type-exists? dependency)
49 | (throw+ {:type ::unexistent-type
50 | :message "An entity type cannot depend on an unexistent entity type"
51 | :entity-type type
52 | :dependency dependency}))))))
53 |
54 | (defn get-sorted-types
55 | "Returns the types in dependency order"
56 | []
57 | (let [types (set (keys (entity/types)))]
58 | (detect-circular-dependencies! types)
59 | (get-sorted-types* [] types)))
60 |
61 | (defn seed-number
62 | [type]
63 | (or (entity/type-property type :seed-number) 30))
64 |
65 | (defn seed
66 | "Migrates the database and transacts the fixtures.
67 | Options:
68 | recreate? - removes the db and creates a new one
69 | generate? - seeds the database with randomly generated entities"
70 | [& {:keys [recreate? generate?]}]
71 | (schema/migrate :recreate? recreate?)
72 | (log/info "Migrations done!")
73 | (doseq [type (get-sorted-types)]
74 | (log/info "Seeding type " type)
75 | (seed-type type (if generate? (seed-number type) 0))))
76 |
--------------------------------------------------------------------------------
/src/scss/ventas/components/_product-list.scss:
--------------------------------------------------------------------------------
1 |
2 | .product-grid {
3 | margin: -14px 0px 40px;
4 |
5 | .ui.grid {
6 | margin: 0px;
7 | }
8 |
9 | .column {
10 | text-align: center;
11 | padding: 1rem !important;
12 | }
13 | }
14 |
15 | .product-grid__product {
16 | color: black;
17 | display: inline-block;
18 | max-width: 350px;
19 | width: 100%;
20 | box-shadow: 0px 0px 7px 0px #bdbdbd;
21 | transition: all 200ms;
22 | background-color: white;
23 |
24 | &:hover {
25 | transform: scale(1.03, 1.03);
26 |
27 | .product-grid__actions {
28 | opacity: 1;
29 | bottom: 0px;
30 | }
31 | }
32 | }
33 |
34 | .product-grid__images-wrapper {
35 | position: relative;
36 |
37 | &--no-image {
38 | .product-grid__actions {
39 | bottom: 0px;
40 | opacity: 1;
41 | position: static;
42 | padding-top: 15px;
43 | font-size: 1.3rem;
44 | padding-bottom: 5px;
45 | }
46 | }
47 | }
48 |
49 | .product-grid__actions {
50 | position: absolute;
51 | width: 100%;
52 | bottom: 4px;
53 | background-color: rgba(255,255,255,0.5);
54 | padding: 10px 0px;
55 | font-size: 1.6rem;
56 | color: black;
57 | transition: all 300ms;
58 | bottom: -20px;
59 | opacity: 0;
60 | text-align: center;
61 |
62 | .icon {
63 | cursor: pointer;
64 | margin: 0px 5px !important;
65 |
66 | &.shopping.bag {
67 | &:hover {
68 | color: #005aff;
69 | }
70 | }
71 |
72 | &.heart {
73 | &:hover {
74 | color: red;
75 | }
76 | }
77 | }
78 | }
79 |
80 | .product-grid__content {
81 | padding: 2px 15px 12px;
82 | text-align: center;
83 | display: block;
84 | color: black;
85 | }
86 |
87 | .product-grid__name {
88 | text-decoration: none;
89 | color: inherit;
90 | display: inline-block;
91 | padding: 5px 0px;
92 | width: 100%;
93 | margin-bottom: 5px;
94 | font-size: 1rem;
95 | }
96 |
97 | .product-grid__price {
98 | font-size: 2rem;
99 | color: inherit;
100 | }
101 |
102 | .product-list__product {
103 | display: flex;
104 | }
105 |
106 | .product-list__content {
107 | padding: 10px 15px;
108 | }
109 |
110 | .product-list__price {
111 | font-size: 1.5rem;
112 | }
113 |
114 | .product-list__actions {
115 | @extend .product-grid__actions;
116 | display: block;
117 | opacity: 1;
118 | position: static;
119 | width: auto;
120 | padding: 0px;
121 | margin-top: 20px;
122 | position: relative;
123 | bottom: 0px;
124 | left: -5px;
125 | }
126 |
127 | .product-list__description {
128 | margin-top: 15px;
129 | }
--------------------------------------------------------------------------------
/doc/Development_workflow.md:
--------------------------------------------------------------------------------
1 | ## Development workflow
2 |
3 | The first thing I do when I want to develop ventas, is bringing up my local environment:
4 |
5 | ```bash
6 | $ docker-compose up -d
7 | ```
8 |
9 | Then, I open Cursive, and start a REPL (just a regular leiningen repl, but inside Cursive).
10 |
11 | When the REPL starts, I load the code and start the mount system:
12 |
13 | ```bash
14 | > (init)
15 | ```
16 |
17 | When I've changed backend code and I want it to be reloaded, I execute this:
18 |
19 | ```clojure
20 | > (repl/r)
21 | ```
22 |
23 | For frontend development I execute:
24 |
25 | ```
26 | shadow-cljs watch :admin
27 | lein sass4clj auto
28 | ```
29 |
30 | ### CLJS gotchas
31 |
32 | #### Devtools custom formatters
33 |
34 | It is very important that you [enable custom formatters in Chrome](https://github.com/binaryage/cljs-devtools/blob/master/docs/installation.md#enable-custom-formatters-in-chrome) to easily debug the application.
35 |
36 | If you don't see this in your console, something is wrong:
37 |
38 | 
39 |
40 |
41 |
42 | ### CLJ gotchas
43 |
44 | #### Exceptions while reloading
45 |
46 | Sometimes, your code has a syntax error, you reload it and, suddenly, it seems you can't do anything anymore:
47 |
48 | ```clojure
49 | (repl/r)
50 | :reloading (repl user ventas.server.api ventas.server.api.admin ventas.server.api.user ventas.server.api.description ventas.plugins.featured-categories.core ventas.plugins.slider.core ventas.plugins.blog.core ventas.plugins.featured-products.core ventas.core ventas.database-test)
51 | :error-while-loading repl
52 | java.lang.RuntimeException: Unable to resolve symbol: doesnotexist
53 | (repl/r)
54 | java.lang.RuntimeException: No such var: repl/r
55 | clojure.lang.Compiler$CompilerException: java.lang.RuntimeException: No such var: repl/r, compiling:(/tmp/form-init1507582247396403063.clj:1:1)
56 |
57 | ```
58 |
59 | The solution to this is moving to the REPL namespace and executing the refresh function:
60 |
61 | ```clojure
62 | (in-ns 'repl)
63 | (tn/refresh)
64 | ;; alternatively...
65 | (clojure.tools.namespace.repl/refresh)
66 | ```
67 |
68 | You can keep reloading this way until you solve the error.
69 |
70 | If that doesn't work, you can try `clojure.tools.namespace.repl/refresh-all`.
71 |
72 | #### Database changes
73 |
74 | You can register a migration or create an entity type to alter the db schema.
75 |
76 | To register a migration:
77 |
78 | ```clojure
79 | (ventas.database.schema/register-migration!
80 | ::my-migration
81 | [{:db/ident :some/stuff
82 | :db/valueType :db.value/string
83 | :db/cardinality :db.cardinality/one}])
84 | ```
85 |
86 | To apply the registered migrations:
87 |
88 | ```clojure
89 | (ventas.database.schema/migrate)
90 | ```
91 |
92 | Entity types are described [here](./Entity_types.md)
93 |
94 | ### SASS
95 |
96 | This project roughly follows BEM.
97 |
98 | To add a new file, you need to add it to `scss/main.scss`.
--------------------------------------------------------------------------------
/src/cljs/ventas/components/crud_table.cljs:
--------------------------------------------------------------------------------
1 | (ns ventas.components.crud-table
2 | "Implementation of ventas.components.table for the most common and boring
3 | use of it."
4 | (:require
5 | [re-frame.core :as rf]
6 | [ventas.i18n :refer [i18n]]
7 | [ventas.components.base :as base]
8 | [ventas.server.api.admin :as api.admin]
9 | [ventas.routes :as routes]
10 | [ventas.components.table :as table]
11 | [ventas.i18n :as i18n]))
12 |
13 | (def state-key ::state)
14 |
15 | (rf/reg-event-fx
16 | ::remove
17 | (fn [_ [_ state-path id]]
18 | {:pre [state-path id]}
19 | {:dispatch [::api.admin/admin.entities.remove
20 | {:params {:id id}
21 | :success [::remove.next state-path id]}]}))
22 |
23 | (rf/reg-event-db
24 | ::remove.next
25 | (fn [db [_ state-path id]]
26 | (update-in db
27 | (conj state-path :rows)
28 | (fn [items]
29 | (remove #(= (:id %) id)
30 | items)))))
31 |
32 | (defn action-column-component [state-path {:keys [id]}]
33 | [:div
34 | [base/button {:icon true
35 | :on-click #(rf/dispatch [::remove state-path id])}
36 | [base/icon {:name "remove"}]]])
37 |
38 | (defn action-column [state-path]
39 | {:id :actions
40 | :label (i18n ::actions)
41 | :component (partial #'action-column-component state-path)
42 | :width 110})
43 |
44 | (defn- footer [edit-route]
45 | [base/button {:on-click #(routes/go-to edit-route :id 0)}
46 | (i18n ::create)])
47 |
48 | (rf/reg-event-fx
49 | ::fetch
50 | (fn [{:keys [db]} [_ entity-type state-path]]
51 | (let [{:keys [page items-per-page sort-direction sort-column]} (table/get-state db state-path)]
52 | {:dispatch [::api.admin/admin.entities.list
53 | {:success [::fetch.next state-path]
54 | :params {:type entity-type
55 | :pagination {:page page
56 | :items-per-page items-per-page}
57 | :sorting {:direction sort-direction
58 | :field sort-column}}}]})))
59 |
60 | (rf/reg-event-db
61 | ::fetch.next
62 | (fn [db [_ state-path {:keys [items total]}]]
63 | (-> db
64 | (assoc-in (conj state-path :rows) items)
65 | (assoc-in (conj state-path :total) total))))
66 |
67 | (rf/reg-event-fx
68 | ::init
69 | (fn [{:keys [db]} [_ state-path {:keys [columns edit-route entity-type]} extra-config]]
70 | {:db (assoc-in db state-path nil)
71 | :dispatch [::table/init state-path
72 | (merge {:fetch-fx [::fetch entity-type]
73 | :columns columns
74 | :footer (partial footer edit-route)}
75 | extra-config)]}))
76 |
77 | (i18n/register-translations!
78 | {:en_US {::actions "Actions"
79 | ::create "Create"
80 | ::submit "Submit"}
81 | :es_ES {::actions "Acciones"
82 | ::create "Nuevo"
83 | ::submit "Enviar"}})
--------------------------------------------------------------------------------
/src/scss/ventas/components/_cart.scss:
--------------------------------------------------------------------------------
1 |
2 | .cart__hover {
3 | font-size: 1rem;
4 | color: black;
5 | font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
6 | font-weight: 700;
7 |
8 | display: none;
9 | position: absolute;
10 | top: calc(100% + 10px);
11 | left: 5px;
12 | background-color: white;
13 | border: solid 1px black;
14 | padding: 0px;
15 | width: 240px;
16 | text-align: left;
17 |
18 | &--visible {
19 | display: block;
20 | }
21 |
22 | button {
23 | width: 100%;
24 | border: 0px;
25 | background-color: grey;
26 | color: white;
27 | padding: 10px 0px;
28 | margin-top: 2px;
29 | transition: background-color 250ms;
30 | &:hover {
31 | background-color: #525252;
32 | }
33 | }
34 |
35 | }
36 |
37 | .cart__hover-item {
38 | padding: 8px 0px;
39 | position: relative;
40 | transition: background-color 250ms;
41 | display: flex;
42 | flex-direction: row;
43 | align-items: center;
44 |
45 | &:nth-child(odd) {
46 | background-color: #f0f0f0;
47 | }
48 | &:hover {
49 | background-color: #e6e6e6;
50 | }
51 | .icon {
52 | cursor: pointer;
53 | position: absolute;
54 | top: 0;
55 | bottom: 0;
56 | right: 4px;
57 | margin: auto;
58 | line-height: 1em;
59 | font-size: 17px;
60 | }
61 | }
62 |
63 | .hover-item__quantity {
64 | margin: 0;
65 | padding: 0px 5px;
66 | }
67 |
68 | .hover-item__name {
69 | padding: 0px 5px;
70 | margin-bottom: 0px;
71 | padding-right: 30px;
72 | }
73 |
74 | .cart__hover-no-items {
75 | padding: 10px;
76 | }
77 |
78 | .cart-notification {
79 | h3 {
80 | font-weight: normal;
81 | }
82 | }
83 |
84 | .cart-notification__image {
85 | width: 35%;
86 | margin-right: 15px;
87 |
88 | img {
89 | width: 100%;
90 | }
91 | }
92 |
93 | .cart-notification__inner {
94 | display: flex;
95 | flex-direction: row;
96 | }
97 |
98 | .cart-notification__info {
99 | flex: 1;
100 |
101 | h4 {
102 | font-weight: normal;
103 | margin: 0px;
104 | margin-bottom: 0.5rem;
105 | }
106 | }
107 |
108 | .cart-notification__terms {
109 | line-height: 0px;
110 | }
111 |
112 | .cart-notification__term {
113 | display: inline-block;
114 | vertical-align: middle;
115 | margin: 3px;
116 | margin-left: 0px;
117 | margin-right: 6px;
118 |
119 | .term {
120 | margin: 0px !important;
121 | }
122 |
123 | .term.term--default {
124 | width: 24px;
125 | height: 24px;
126 | line-height: 24px;
127 |
128 | h3 {
129 | font-size: 1rem;
130 | }
131 | }
132 |
133 | .term.term--color {
134 | width: 20px;
135 | height: 20px;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------