(import-macros {:compile-html HTML} :macros) (local lib (require :lib)) (local shop (require :shop)) (local dicts (require :dicts)) (local templates (require :templates)) (local synonyms (require :synonyms)) (local pagination-limit 12) (fn all-products [db page filters] (local where-stmts []) (local where-args []) (var has-search-query? false) (when filters.only-published? (table.insert where-stmts "products.published = true")) (when filters.type (table.insert where-stmts "products.type = ?") (table.insert where-args filters.type)) (when filters.search (table.insert where-stmts "products_search MATCH ?") (var query (synonyms.replace filters.search)) (when (not (lib.ends-with? query "*")) (set query (.. query "*"))) (table.insert where-args query) (set has-search-query? true)) (local where (if (< 0 (# where-stmts)) (.. "WHERE " (table.concat where-stmts " AND\n")) "")) (local from-sql (if has-search-query? "products_search INNER JOIN products ON products_search.name = products.name" "products")) {:total (. (_G.must (luna.db.query db (.. "SELECT COUNT(products.ROWID) FROM " from-sql " " where) where-args)) 1 1) :products (_G.must (luna.db.query-assoc db (.. "SELECT products.name, products.title, products.published, products.short_description as \"short-description\", products.price_per AS \"price-per\", products.volume, products.stock, products.packaging, products.type, products.region, products.image1, products.image2, products.image3, products.image4, products.image5 FROM " from-sql " " where " ORDER BY products.position ASC, products.creation_time DESC" (if has-search-query? ", rank" "") " LIMIT ? OFFSET ?") (lib.concat where-args [pagination-limit (* (- page 1) pagination-limit)])))}) (fn filters-path [page filters] (.. "/shop?page=" (tostring page) (if filters.type (.. "&type=" filters.type) "") (if filters.search (.. "&search=" filters.search) ""))) (fn paginator-template [filters page limit total classes] (if (< limit total) (HTML [:div {:class (.. "paginator " classes)} (if (< 1 page) (HTML [:div {} [:a {:href (filters-path (- page 1) filters)} "⟵ Предыдущая страница"]]) "") (if (< page (math.ceil (/ total limit))) (HTML [:div {} [:a {:href (filters-path (+ page 1) filters)} "Следующая страница ⟶"]]) "")]) "")) (fn item-template [product basket redirect-url] (local item-url (.. "/shop/" product.name)) (local images []) (for [i 2 5] (table.insert images (. product (.. "image" i)))) (HTML [:section {:class (.. "shop-item" (if (not product.published) " shop-item-not-published" ""))} [:a {:href item-url} [:div {:class "shop-item-imgs"} [:img {:class "shop-item-img" :src (.. "/static/files/" (. product.image1) "-thumbnail.jpg")}] (table.concat (let [without-videos (icollect [_ v (ipairs images)] (if (lib.ends-with? (_G.must (luna.utf8.lower v)) ".webm") nil v))] (icollect [idx image (ipairs without-videos)] (HTML [:img {:class "shop-item-img" :src (.. "/static/files/" image "-thumbnail.jpg") :loading "lazy" :style (.. "z-index: " (+ idx 2) ";" "width: calc(100% / " (# without-videos) ");" "left: calc(100% / " (# without-videos) " * " (- idx 1) ")")}]))))]] [:a {:href item-url} [:h3 {:class "shop-item-title"} product.title]] [:div {:class "shop-item-price"} (templates.add-to-basket-form product basket "" redirect-url)] (templates.product-overview product "mb-0-25 font-size-0-875") [:div {} product.short-description]])) (fn content [db products page total filters basket authenticated?] (local redirect-url (filters-path page filters)) [(HTML [:aside {} (templates.header "/shop" authenticated?) (if (< 0 (# basket)) (templates.basket basket redirect-url) "") ;; [:section {} [:h2 {} "Условия"] [:p {} ""]] ]) (HTML [:div {:class "content"} [:div {:class "back"} [:a {:href "/"} "⟵ Обратно на главную"]] [:h2 {:class "product-page-title"} "Магазин"] (if authenticated? (HTML [:div {:class "mb-1" :style "margin-top: -0.5rem"} [:a {:style "white-space: nowrap" :href (.. "/shop/add")} "+ Добавить"] [:a {:style "white-space: nowrap; margin-left: 1rem;" :href (.. "/shop/order/list")} "☰ Список заказов"]]) "") [:form {:class "d-flex-desktop gap-0-5 mb-1"} [:input {:name "search" :type "search" :placeholder "Поиск" :value (or filters.search "")}] [:select {:name "type"} [:option {:value ""} "Все товары"] (table.concat (icollect [_ v (ipairs dicts.product-type)] (HTML [:option (fn [] {:value v.value :selected (= filters.type v.value)}) v.label])))] [:button {:type "submit"} "Применить"]] (paginator-template filters page pagination-limit total "mb-1") [:div {:class "shop-items"} (if (< 0 (# products)) (table.concat (icollect [_ v (ipairs products)] (item-template v basket redirect-url))) (HTML [:em {} "Пока что здесь ничего нет!"]))] (paginator-template filters page pagination-limit total "mt-2")])]) (fn render [request db authenticated?] (let [order-id (shop.order-id request) basket (if order-id (shop.basket db order-id) []) type (if (and request.query.type (not (lib.empty? (. request.query.type 1)))) (. request.query.type 1) nil) search (if (and request.query.search (not (lib.empty? (. request.query.search 1)))) (. request.query.search 1) nil) page (if (and request.query.page (not (lib.empty? (. request.query.page 1)))) (math.max 1 (tonumber (. request.query.page 1))) 1) only-published? (not authenticated?) filters {: type : search : page : only-published?} {: total : products} (all-products db page filters)] (values 200 {} (templates.base (content db products page total filters basket authenticated?))))) {: render}