├── 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 | ![devtools](./devtools.png) 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 | --------------------------------------------------------------------------------