diff options
| author | unwox <me@unwox.com> | 2025-08-31 17:51:57 +0600 |
|---|---|---|
| committer | unwox <me@unwox.com> | 2025-09-04 20:14:11 +0600 |
| commit | 66c51b0e714fa8a1c80784108191270babc8525e (patch) | |
| tree | 0640549f522092096d83c78b9be9b1fa4a03929e /pages | |
| parent | d8039a77d582f696ab98b2a6d02ce924fbacfa41 (diff) | |
implement shop
Diffstat (limited to 'pages')
| -rw-r--r-- | pages/auth.fnl | 68 | ||||
| -rw-r--r-- | pages/index.fnl | 130 | ||||
| -rw-r--r-- | pages/shop/_product/edit.fnl | 75 | ||||
| -rw-r--r-- | pages/shop/_product/index.fnl | 83 | ||||
| -rw-r--r-- | pages/shop/add.fnl | 83 | ||||
| -rw-r--r-- | pages/shop/cart/add.fnl | 38 | ||||
| -rw-r--r-- | pages/shop/cart/remove.fnl | 23 | ||||
| -rw-r--r-- | pages/shop/index.fnl | 155 | ||||
| -rw-r--r-- | pages/shop/order.fnl | 61 | ||||
| -rw-r--r-- | pages/shop/success.fnl | 15 | ||||
| -rw-r--r-- | pages/staff/before-leaving.fnl | 36 |
11 files changed, 651 insertions, 116 deletions
diff --git a/pages/auth.fnl b/pages/auth.fnl new file mode 100644 index 0000000..fc35be0 --- /dev/null +++ b/pages/auth.fnl @@ -0,0 +1,68 @@ +(import-macros {:compile-html <>} :macros) +(local forms (require :forms)) +(local lib (require :lib)) +(local templates (require :templates)) + +(local auth-form [ + {:title "" + :fields [ + (forms.text-input "name" "Пользователь" true) + (forms.password-input "password" "Пароль" true)]}]) + +(fn content [data errors] + (set data.password nil) + [(<> + [:div {:class "side"} + (templates.header "/auth")]) + (<> + [:section {:class "content"} + [:div {:class "mb-1"} [:a {:href "/"} "⟵ Обратно на главную"]] + [:h2 {} "Войти"] + (forms.render-form auth-form data errors)])]) + +(fn create-session [db user] + (local id (_G.must (luna.crypto.random-string 64))) + (local next-week (os.date "%Y-%m-%d %H:%M:%S" + (+ (os.time) (* 60 60 24 7)))) + (_G.must + (luna.db.exec + db "INSERT INTO auth_sessions (id, user, creation_time, expires_at) + VALUES (?, ?, ?, ?)" + [id user (lib.now) next-week])) + + id) + +(fn check-user [db name entered-pass] + (local users + (_G.must + (luna.db.query db + "SELECT users.password + FROM USERS + WHERE LOWER(users.name) = ? + LIMIT 1" [name]))) + + (if (< 0 (# users)) + (let [password (. users 1 1)] + (_G.must (luna.crypto.check-password entered-pass password))) + false)) + +(fn render [request db authenticated?] + (if authenticated? + (values 302 {:Location "/"} "") + (if request.form + (let [name request.form.name + entered-password request.form.password + correct-creds? (check-user db name entered-password) + errors (if (not correct-creds?) + {:password "Пользователя с таким именем и паролем не существует."} + nil)] + (if correct-creds? + (do + (local session-id (create-session db name)) + (values 302 {:Location "/shop" + :Set-Cookie (.. "auth= " session-id "; HttpOnly; SameSite=strict" + (if luna.debug? "" "; Secure"))} "")) + (values 200 {} (templates.base (content request.form errors))))) + (values 200 {} (templates.base (content {} {})))))) + +{: render} diff --git a/pages/index.fnl b/pages/index.fnl index 4d8af80..f0ac111 100644 --- a/pages/index.fnl +++ b/pages/index.fnl @@ -1,101 +1,71 @@ +(import-macros {:compile-html <>} :macros) +(local lib (require :lib)) (local templates (require :templates)) -(local html (require :vendor.html)) - -(fn improve-typography [text] - (var result - (-> text - (string.gsub "(\n|\r)" " ") - (string.gsub "%s+" " "))) - (let [nbsp-replaces ["на" "На" "и" "И" "в" "В" "о" "О" "с" "С" "со" "Со" "до" - "До" "для" "Для" "а" "А" "но" "Но" "на" "На" "я" "Я" "мы" - "Мы" "над" "Над" "под" "Под" "г." "Г." "ул." "Ул." - "д." "Д." "%d+"]] - (each [_ v (ipairs nbsp-replaces)] - (set result - (-> result - (string.gsub (.. "( " v ") ") "%1 ") - (string.gsub (.. "(%s" v ") ") " %1 ") - (string.gsub (.. "^(" v ") ") "%1 "))))) - result) - -(fn test-improve-typography [] - (assert (= (improve-typography "Я лежу на пляжу!") - "Я лежу на пляжу!")) - (assert (= (improve-typography "500 рублей мы сняли со счета!") - "500 рублей мы сняли со счета!")) - (assert (= (improve-typography "500 рублей мы - сняли со счета!") - "500 рублей мы сняли со счета!"))) (local texts { :address - (improve-typography + (lib.improve-typography "г. Омск, ул. Пушкина, д. 133/9, этаж 2. Вход с крыльца Магнита, дверь - слева, домофон 4") + слева, домофон 4. Дверь в офисе узнаете по нашему логотипу.") :individual-ceremony - (improve-typography + (lib.improve-typography "Индивидуальная чайная церемония: мастер готовит для вас чай на ваш выбор. О времени встречи договариваемся. Стоимость 1000 рублей с человека, до 5 человек.") :weekly-meetings - (improve-typography + (lib.improve-typography "Еженедельное мероприятие: каждую субботу в 15:00 мы собираемся и пьем чай из нашей коллекции. Для посещения необходима запись в комментариях - под соответствующим постом в нашей группе в телеграме. Стоимость 500 - рублей с человека.") -}) + под соответствующим постом в нашей группе. Стоимость 500 рублей + с человека.")}) (fn pick-gallery-photo [list] (let [chosen (. list (math.random (# list)))] - [:div {} - [:a {:href (.. "static/" chosen.name ".webp") :target "_blank"} - [:img {:src (.. "static/" chosen.name "-p.webp") :alt chosen.alt}]]])) + (<> + [:div {} + [:a {:href (.. "static/" chosen.name ".webp") :target "_blank"} + [:img {:src (.. "static/" chosen.name "-p.webp") :alt chosen.alt}]]]))) -(fn content [] - [[:div {:class "content"} - [:article {:class "article"} - [:img {:class "logo" :src "/static/logo.svg" - :alt "Белая жаба в мультяшном стиле с чайником на голове"}] - [:h1 {} [:NO-ESCAPE "Чайная комната «Белая жаба»"]] - [:nav {} - [:a {:href "https://t.me/whitetoadtea"} "телеграм"] - [:span {} "~"] - [:a {:href "https://vk.com/whitetoadtea"} "вконтакте"]]] - [:article {:class "article"} - [:h2 {} "Адрес"] - [:p {} [:NO-ESCAPE texts.address]]] - [:article {:class "article"} - [:h2 {} "Форматы участия"] - [:ol {} - [:li {} - [:NO-ESCAPE texts.individual-ceremony] - [:div {:class "button-wrapper"} - [:a {:href "https://t.me/whitetoadvlad" :class "button"} "Записаться"]]] - [:li {} - [:NO-ESCAPE texts.weekly-meetings] - [:div {:class "button-wrapper"} - [:a {:href "https://t.me/whitetoadtea" :class "button"} "Подписаться"]]]]]] - [:div {:class "gallery"} - (pick-gallery-photo [{:name "people" :alt "Люди в чайной"} - {:name "gaiwan2" :alt "Близко сфотографированный чайный столик с пиалами и гайванью"} - {:name "teapot2" :alt "Пиала и чайник на фоне гирлянды"}]) - (pick-gallery-photo [{:name "table" :alt "Сфотографированные сверху пиалы на японском столике"}]) - [:div {} - [:video {:autoplay true :loop true :muted true} - [:source {:src "static/boiling-p.webm" :type "video/webm"}]]] - (pick-gallery-photo [{:name "gaiwan" :alt "Гайвань и ширма с гирляндами на фоне"} - {:name "wall" :alt "Белая стена с веером и тенью окна"} - {:name "teapot" :alt "Чайник в темноте на светлом фоне гирлянд"} - {:name "teapot3" :alt "Чайник в руке на фоне растений"}]) - (pick-gallery-photo [{:name "rack" :alt "Стеллаж с чайной посудой"} - {:name "flowers" :alt "Ваза с цветами рядом с чайным столиком"}]) - [:div {} - [:video {:autoplay true :loop true :muted true} - [:source {:src "static/gaiwan-p.webm" :type "video/webm"}]]]]]) +(fn content [authenticated?] + [(<> + [:div {:class "side mb-2"} + (templates.header "" authenticated?) + [:section {:class "mb-2"} + [:h2 {} "Адрес"] + [:p {} [:NO-ESCAPE texts.address]]] + [:section {} + [:h2 {} "Форматы участия"] + [:div {:class "mb-2"} + [:div {:class "mb-1"} [:NO-ESCAPE texts.individual-ceremony]] + [:div {} + [:a {:href "https://t.me/whitetoadvlad"} "Записаться ⟶"]]] + [:div {} + [:div {:class "mb-1"} [:NO-ESCAPE texts.weekly-meetings]] + [:div {} + [:a {:href "https://t.me/whitetoadtea"} "Подписаться ⟶"]]]]]) + (<> + [:div {:class "content"} + [:div {:class "gallery"} + (pick-gallery-photo [{:name "people" :alt "Люди в чайной"} + {:name "gaiwan2" :alt "Близко сфотографированный чайный столик с пиалами и гайванью"} + {:name "teapot2" :alt "Пиала и чайник на фоне гирлянды"}]) + (pick-gallery-photo [{:name "table" :alt "Сфотографированные сверху пиалы на японском столике"}]) + [:div {} + [:video {:autoplay true :loop true :muted true} + [:source {:src "static/boiling-p.webm" :type "video/webm"}]]] + (pick-gallery-photo [{:name "gaiwan" :alt "Гайвань и ширма с гирляндами на фоне"} + {:name "wall" :alt "Белая стена с веером и тенью окна"} + {:name "teapot" :alt "Чайник в темноте на светлом фоне гирлянд"} + {:name "teapot3" :alt "Чайник в руке на фоне растений"}]) + (pick-gallery-photo [{:name "rack" :alt "Стеллаж с чайной посудой"} + {:name "flowers" :alt "Ваза с цветами рядом с чайным столиком"}]) + [:div {} + [:video {:autoplay true :loop true :muted true} + [:source {:src "static/gaiwan-p.webm" :type "video/webm"}]]]]])]) -(fn render [request] - (values 200 {} (html.render (templates.base-template (content)) true))) +(fn render [request _ authenticated?] + (values 200 {} (templates.base (content authenticated?)))) {: render} diff --git a/pages/shop/_product/edit.fnl b/pages/shop/_product/edit.fnl new file mode 100644 index 0000000..9465d94 --- /dev/null +++ b/pages/shop/_product/edit.fnl @@ -0,0 +1,75 @@ +(import-macros {:compile-html <>} :macros) +(local templates (require :templates)) +(local {: product-form} (require :pages.shop.add)) +(local forms (require :forms)) +(local lib (require :lib)) + +(fn find-product [db name] + (. + (_G.must + (luna.db.query-assoc + db + "SELECT products.published, + products.name, + products.title, + products.position, + products.short_description, + products.stock, + products.type, + products.packaging, + products.description, + products.price_per, + products.stock, + products.vendor, + products.vendor_article, + products.vendor_description, + products.vendor_price_per, + products.vendor_product_link, + products.image1, + products.image2, + products.image3, + products.image4, + products.image5 + FROM products + WHERE products.name = ?" + [name])) + 1)) + +(fn update-product [tx form data where] + (let [sql-and-args (forms.form-update-sql-statement "products" form data {} where)] + (if sql-and-args + (_G.must (luna.db.exec-tx tx (table.unpack sql-and-args))) + (error "empty data for insert SQL-statement")))) + +(fn content [form data errors authenticated?] + [(<> + [:div {:class "side"} + (templates.header "/shop" authenticated?)]) + (<> + [:div {:class "content"} + [:div {:class "mb-1"} + [:a {:href (.. "/shop/" data.name)} "⟵ Обратно к товару"]] + [:h2 {} "Редактировать товар"] + (forms.render-form form data errors)])]) + +(fn render [request db authenticated?] + (if (not authenticated?) + (values 302 {:Location "/shop"} "") + (if request.form + (let [data (forms.convert-values-from-html product-form request.form db) + errors (forms.validate-form product-form data) + has-errors? (not (lib.empty-table? errors))] + (if has-errors? + (values 400 {} (templates.base (content product-form data errors + authenticated?))) + (do + (lib.with-tx db + (fn [tx] (update-product tx product-form data + {:name request.params._product}))) + (values 302 {:Location (.. "/shop/" data.name)} "")))) + (values 200 {} + (templates.base + (content product-form (find-product db request.params._product) {} + authenticated?)))))) + +{: render } diff --git a/pages/shop/_product/index.fnl b/pages/shop/_product/index.fnl new file mode 100644 index 0000000..722c952 --- /dev/null +++ b/pages/shop/_product/index.fnl @@ -0,0 +1,83 @@ +(import-macros {:compile-html <>} :macros) +(local templates (require :templates)) +(local dicts (require :dicts)) +(local lib (require :lib)) + +(fn text->html [text] + (assert (= (type text) "string")) + (var result "") + (var from 1) + (var to (text:find "\n%s*\n%s*" from)) + (while to + (set result (.. result "<p>" (text:sub from (- to 1)) "</p>\n")) + (set from (+ to 2)) + (set to (text:find "\n%s*\n%s*" from))) + (.. result "<p>" (text:sub from) "</p>")) + +(fn find-product [db name] + (. + (_G.must + (luna.db.query-assoc + db + "SELECT products.name, + products.title, + products.description, + products.price_per AS \"price-per\", + products.type, + products.stock, + products.published, + products.image1, + products.image2, + products.image3, + products.image4, + products.image5 + FROM products + WHERE products.name = ?" + [name])) + 1)) + +(fn content [product authenticated?] + (local images []) + (for [i 1 5] + (table.insert images (. product (.. "image" i)))) + + [(<> + [:div {:class "side"} + (templates.header "/shop" authenticated?)]) + (<> + [:div {:class "content"} + [:div {:class "mb-1"} [:a {:href "/shop"} "⟵ Обратно к списку"]] + + (let [link (.. "/static/files/" product.image1)] + (<> [:a {:href link :target "_blank"} + [:img {:class "product-page-img-mobile mb-1-5" + :src (.. link "-thumbnail.jpg")}]])) + [:div {:class "product-page-layout"} + [:div {} + [:h2 {:class "product-page-title mb-1"} product.title] + [:section {:class "mb-2"} + (if authenticated? + (<> [:div {:class "mb-0-5"} + [:a {:href (.. "/shop/" product.name "/edit")} + "✎ Редактировать"]]) + "") + [:div {:class "mb-0-5" :style "font-style: italic;"} + (or (dicts.label dicts.tea-type product.type) product.type) ", " + [:strong {} (* 50 product.price-per) "₽ за 50 грамм "] + (.. "(" product.price-per "₽ за 1 грамм)")]] + [:div {:class "mb-2"} "~~~"] + [:NO-ESCAPE (text->html product.description)]] + [:div {:class "product-page-imgs"} + (table.concat + (icollect [idx image (ipairs images)] + (let [link (.. "/static/files/" image)] + (<> [:a {:href link :target "_blank"} + [:img {:class "product-page-img" :src (.. link "-thumbnail.jpg")}]]))))]]])]) + +(fn render [request db authenticated?] + (let [product (find-product db request.params._product)] + (if (and product (or product.published authenticated?)) + (values 200 {} (templates.base (content product authenticated?))) + (values 404 {} "not found")))) + +{: render} diff --git a/pages/shop/add.fnl b/pages/shop/add.fnl new file mode 100644 index 0000000..8fcfdf1 --- /dev/null +++ b/pages/shop/add.fnl @@ -0,0 +1,83 @@ +(import-macros {:compile-html <>} :macros) +(local templates (require :templates)) +(local dicts (require :dicts)) +(local forms (require :forms)) +(local lib (require :lib)) + +(local product-form + [{:title "" + :fields [ + (forms.checkbox-input "published" "Опубликован" false + "Отображать ли товар на страницах магазина.") + (forms.number-input "position" "Позиция в списке" true 0 1000 + "Чем выше число, тем позже в списке будет находиться товар.") + (forms.text-input "name" "Алиас" true 0 125 + (.. "Уникальное название чая на латинице, без пробелов, " + "в нижнем регистре.")) + (forms.text-input "title" "Полное название" true 0 200) + (forms.select-input "type" "Вид чая" true dicts.tea-type) + (forms.select-input "packaging" "Упаковка" true dicts.tea-packaging) + (forms.textarea-input "short_description" "Короткое описание" true 0 1000) + (forms.textarea-input "description" "Полное описание" true 0 20000) + (forms.number-input "price_per" "Цена" true 0 100000 + "За штуку или грамм.") + (forms.number-input "stock" "Сколько в наличии" true 0 100000 + "В штуках или граммах.")]} + + {:title "Фото" + :fields [ + (forms.file-input "image1" "Первое" true ".jpg" 512) + (forms.file-input "image2" "Второе" false ".jpg" 512) + (forms.file-input "image3" "Третье" false ".jpg" 512) + (forms.file-input "image4" "Четвертое" false ".jpg" 512) + (forms.file-input "image5" "Пятое" false ".jpg" 512)]} + + {:title "Данные о поставщике" + :fields [ + (forms.select-input "vendor" "Поставщик" false + [{:value "oz" :label "Орехово-Зуево"} + {:value "chaibez" :label "Чай без церемоний"}]) + (forms.text-input "vendor_article" "Артикль или ID товара у поставщика" false + 0 50) + (forms.textarea-input "vendor_description" "Описание товара от поставщика" + false 0 10000) + (forms.number-input "vendor_price_per" "Цена поставщика" false 0 100000 + "За штуку или грамм.") + (forms.url-input "vendor_product_link" "Ссылка на чай у поставщика" false)]}]) + +(fn insert-product [tx form data] + (let [sql-and-args (forms.form-insert-sql-statement "products" form data + {:creation_time (lib.now)})] + (if sql-and-args + (_G.must (luna.db.exec-tx tx (table.unpack sql-and-args))) + (error "empty data for insert SQL-statement")))) + +(fn content [form data errors authenticated?] + [(<> + [:div {:class "side"} + (templates.header "/shop" authenticated?)]) + (<> + [:div {:class "content"} + [:div {:class "mb-1"} + [:a {:href "/shop"} "⟵ Обратно к списку"]] + [:h2 {} "Добавить товар"] + (forms.render-form form data errors)])]) + +(fn render [request db authenticated?] + (if (not authenticated?) + (values 302 {:Location "/shop"} "") + (if request.form + (let [data (forms.convert-values-from-html product-form request.form db) + errors (forms.validate-form product-form request.form) + has-errors? (not (lib.empty-table? errors))] + (if has-errors? + (values 200 {} (templates.base (content product-form request.form errors + authenticated?))) + (do + (lib.with-tx db + (fn [tx] (insert-product tx product-form request.form))) + (values 302 {:Location "/shop"} "")))) + (values 200 {} (templates.base (content product-form {} {} + authenticated?)))))) + +{: render : product-form} diff --git a/pages/shop/cart/add.fnl b/pages/shop/cart/add.fnl new file mode 100644 index 0000000..36e3e41 --- /dev/null +++ b/pages/shop/cart/add.fnl @@ -0,0 +1,38 @@ +(local lib (require :lib)) + +(fn create-order [db] + (let [id (_G.must (luna.crypto.random-string 64))] + (_G.must + (luna.db.exec + db "INSERT INTO orders (id, creation_time) VALUES (?, ?)" + [id (lib.now)])) + id)) + +(fn create-order-line [db order-id name quantity] + (_G.must + (luna.db.exec + db + "INSERT INTO order_lines (order_id, product_name, quantity) VALUES (?, ?, ?)" + [order-id name quantity]))) + +(fn render [request db] + (if (= request.method "POST") + (do + (var order-id (lib.order-id request)) + (var headers + (if (not order-id) + (do + (set order-id (create-order db)) + {:Set-Cookie (.. "order= " order-id "; HttpOnly; SameSite=strict" + (if luna.debug? "" "; Secure"))}) + {})) + + (if (and order-id request.body) + (let [body-values (lib.parse-values request.body)] + (create-order-line db order-id body-values.name body-values.quantity) + (tset headers :Location "/shop") + (values 302 headers "")) + (values 400 {} "bad body"))) + (values 404 {} "not found"))) + +{: render} diff --git a/pages/shop/cart/remove.fnl b/pages/shop/cart/remove.fnl new file mode 100644 index 0000000..d5e3531 --- /dev/null +++ b/pages/shop/cart/remove.fnl @@ -0,0 +1,23 @@ +(local lib (require :lib)) + +(fn render [request db] + (if (= request.method "POST") + (let [order-id (lib.order-id request)] + (if (and order-id request.body) + (do + (local body-values (lib.parse-values request.body)) + (_G.must + (luna.db.exec + db + "DELETE FROM order_lines WHERE id = ? AND order_id = ?" + [body-values.id order-id])) + (values + 302 + {:Location (_G.must + (luna.http.decode-url + (or body-values.redirect-url "/shop")))} + "")) + (values 400 {} "bad body"))) + (values 404 {} "not found"))) + +{: render} diff --git a/pages/shop/index.fnl b/pages/shop/index.fnl new file mode 100644 index 0000000..5a96ab2 --- /dev/null +++ b/pages/shop/index.fnl @@ -0,0 +1,155 @@ +(import-macros {:compile-html <>} :macros) +(local lib (require :lib)) +(local dicts (require :dicts)) +(local templates (require :templates)) + +(fn all-products [db authenticated?] + (local where + (if (not authenticated?) + "WHERE products.published = true" + "")) + (_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.type, + products.image1, + products.image2, + products.image3, + products.image4, + products.image5 + FROM products " + where + " ORDER BY products.position") []))) + +(fn quantity-steps [stock step] + (assert (< 0 step) "step must be greater than 0") + + (var result []) + (var first (math.min stock step)) + (while (<= first stock) + (table.insert result first) + (set first (+ first step))) + result) + +(fn item-template [product] + (local item-url (.. "/shop/" product.name)) + ;; (var quantity-options []) + ;; (if (< 0 product.stock) + ;; (each [_ q (ipairs (quantity-steps product.stock 50))] + ;; (table.insert quantity-options + ;; (<> + ;; [:option {:value (tostring q)} + ;; (.. q " грамм за " (* product.price-per q) "₽")]))) + ;; (table.insert quantity-options (<> [:option {:value "0"} "Товар закончился"]))) + + (local images []) + (for [i 2 5] + (table.insert images (. product (.. "image" i)))) + + (<> + [: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 + (icollect [idx image (ipairs images)] + (<> + [:img {:class "shop-item-img" :src (.. "/static/files/" image "-thumbnail.jpg") + :loading "lazy" + :style (.. "z-index: " (+ idx 2) ";" + "width: calc(100% / " (# images) ");" + "left: calc(100% / " (# images) " * " (- idx 1) ")")}])))]] + [:a {:href item-url} [:h3 {:class "shop-item-title"} product.title]] + [:div {:style "font-style: italic; margin-bottom: 0.25rem;"} + (or (dicts.label dicts.tea-type product.type) product.type) ", " + [:strong {} (* 50 product.price-per) "₽ за 50 гр. "]] + ;; [:div {:class "shop-item-price"} + ;; [:form {:method "POST"} + ;; [:input {:type "hidden" :name "name" :value product.name}] + ;; [:select {:name "quantity"} (table.concat quantity-options)] + ;; [:button {:type "submit"} "Добавить"]]] + [:div {} product.short-description]])) + +(fn content [db basket basket-total authenticated?] + [(<> + [:div {:class "side"} + (templates.header "/shop" authenticated?) + (if (< 0 (# basket)) + (<> + [:article {:class "article"} + [:h2 {} "Корзина"] + [:div {} + (table.concat + (icollect [_ item (ipairs basket)] + (templates.basket-item item "/shop")))] + [:div {} "~~~"] + [:div {:class "basket-total"} (.. "Итого: " basket-total "₽")] + [:a {:href "/shop/order"} "Оформить заказ"]]) + "")]) + (<> + [:div {:class "content"} + [:div {:class "mb-1"} [:a {:href "/"} "⟵ Обратно на главную"]] + [:h2 {:class "mb-1 product-page-title"} + "Магазин" + (if authenticated? + (<> [:a {:style "font-size: 1rem; margin-left: 0.75rem;" + :href (.. "/shop/add")} "+ Добавить"]) + "")] + [:div {:class "shop-items"} + (let [products (all-products db authenticated?)] + (if (< 0 (# products)) + (table.concat + (icollect [_ v (ipairs products)] + (item-template v))) + (<> [:em {} "Пока что здесь ничего нет!"])))]])]) + +(fn create-order [db] + (let [id (_G.must (luna.crypto.random-string 64))] + (_G.must + (luna.db.exec + db "INSERT INTO orders (id, creation_time) VALUES (?, ?)" + [id (lib.now)])) + id)) + +(fn create-order-line [db order-id name quantity] + (_G.must + (luna.db.exec + db + "INSERT INTO order_lines (order_id, product_name, quantity) VALUES (?, ?, ?)" + [order-id name quantity]))) + +(fn render [request db authenticated?] + (let [order-id (lib.order-id request) + basket (if order-id (lib.basket db order-id) []) + basket-total (accumulate [sum 0 _ v (ipairs basket)] + (+ sum (* v.quantity v.price-per)))] + (if (= request.method "POST") + (do + (var order-id (lib.order-id request)) + (var headers + (if (not order-id) + (do + (set order-id (create-order db)) + {:Set-Cookie (.. "order= " order-id "; HttpOnly; SameSite=strict" + (if luna.debug? "" "; Secure"))}) + {})) + + (if (and order-id request.body) + (let [body-values (lib.parse-values request.body)] + (create-order-line db order-id body-values.name body-values.quantity) + (tset headers :Location "/shop") + (values 302 headers "")) + (values 400 {} "bad body"))) + (values + 200 {} + (templates.base (content db basket basket-total authenticated?)))))) + +{: render} diff --git a/pages/shop/order.fnl b/pages/shop/order.fnl new file mode 100644 index 0000000..6edaf8a --- /dev/null +++ b/pages/shop/order.fnl @@ -0,0 +1,61 @@ +(local lib (require :lib)) +(local templates (require :templates)) +(local html (require :vendor.html)) + +(fn content-template [db basket basket-total] + [[:div {:class "side"} + (templates.header "/shop/order")] + [:div {:class "content"} + (if (< 0 (# basket)) + [:section {} + [:h2 {} "Состав заказа"] + [:div {} + (table.unpack + (icollect [_ item (ipairs basket)] + (templates.basket-item item "/shop/order")))] + [:div {} "~~~"] + [:div {:class "basket-total"} (.. "Итого: " basket-total "₽")]] + "") + [:section {} + [:h2 {} "Данные для связи"] + [:form {:class "form" :method "POST"} + [:div {:class "form-row"} + [:label {:for "name"} "Имя"] + [:input {:type "text" :id "name" :name "name" :required "required"}]] + [:div {:class "form-row"} + [:label {:for "contact"} "Телеграм или Email для связи"] + [:input {:type "text" :id "contact" :name "contact" :required "required"}]] + [:div {:class "form-row"} + [:input {:type "checkbox" :id "everything-is-correct" + :name "everything-is-correct" :required "required"}] + [:label {:for "everything-is-correct"} "Данные заказа верны"]] + [:div {:class "form-row"} + [:input {:type "checkbox" :id "agree-to-conditions" + :name "agree-to-conditions" :required "required"}] + [:label {:for "agree-to-conditions"} "Согласен с условиями"]] + [:button {:type "submit"} "Оформить заказ"]]]]]) + +(fn place-order [db order-id form] + (_G.must + (luna.db.exec db + "UPDATE orders SET placement_time = ?, first_name = ?, contact = ?" + [(os.date "%Y-%m-%d %H:%M:%S") form.name form.contact]))) + +(fn render [request db] + (let [order-id (lib.order-id request) + basket (if order-id (lib.basket db order-id) []) + basket-total (accumulate [sum 0 _ v (ipairs basket)] + (+ sum (* v.quantity v.price-per)))] + (if (= request.method "POST") + (do + (place-order db order-id (lib.parse-values request.body)) + (values 302 {:Location "/shop/success"} "")) + (if (< 0 (# basket)) + (values + 200 {} + (html.render + (templates.base (content-template db basket basket-total)) + true)) + (values 302 {:Location "/shop"} ""))))) + +{: render} diff --git a/pages/shop/success.fnl b/pages/shop/success.fnl new file mode 100644 index 0000000..2e9abb4 --- /dev/null +++ b/pages/shop/success.fnl @@ -0,0 +1,15 @@ +(local templates (require :templates)) +(local html (require :vendor.html)) + +(tset _G :package :loaded "pages.shop.success" nil) + +(fn content [] + [[:div {:class "side"} + (templates.header "/shop/order")] + [:div {:class "content"} + "Спасибо за заказ!"]]) + +(fn render [] + (values 200 {} (html.render (templates.base (content)) true))) + +{: render} diff --git a/pages/staff/before-leaving.fnl b/pages/staff/before-leaving.fnl deleted file mode 100644 index b358bdf..0000000 --- a/pages/staff/before-leaving.fnl +++ /dev/null @@ -1,36 +0,0 @@ -(local templates (require :templates)) -(local html (require :vendor.html)) - -(local content - [[:article {:class "article"} - [:img {:class "logo" :src "/static/logo.svg" - :alt "Белая жаба в мультяшном стиле с чайником на голове"}] - [:h1 {} [:NO-ESCAPE "Чайная «Белая жаба»"]] - [:nav {} - [:a {:href "https://t.me/whitetoadtea"} "телеграм"] - [:span {} "~"] - [:a {:href "https://vk.com/whitetoadtea"} "вконтакте"]] - [:p {} - (table.unpack - (icollect [_ v (ipairs ["Затушить свечи" - "Убрать чай" - "Промыть, протереть и убрать посуду" - "Слить и протереть чабань" - "Протереть стол" - "Слить чайник" - "Слить ведро" - "Закрыть окна" - "Вытащить электронику из розеток" - "Выключить сетевой фильтр" - "Выключить гирлянды" - "Закрыть дверь в офис" - "Поставить на сигнализацию" - "Закрыть дверь в офисы (если уходим последние)"])] - [:div {} - [:label {:class "form-row"} - [:input {:type "checkbox"}] v]]))]]]) - -(fn render [request] - (values 200 {} (html.render (templates.base-template content) true))) - -{: render} |
