summaryrefslogtreecommitdiff
path: root/pages
diff options
context:
space:
mode:
Diffstat (limited to 'pages')
-rw-r--r--pages/auth.fnl68
-rw-r--r--pages/index.fnl130
-rw-r--r--pages/shop/_product/edit.fnl75
-rw-r--r--pages/shop/_product/index.fnl83
-rw-r--r--pages/shop/add.fnl83
-rw-r--r--pages/shop/cart/add.fnl38
-rw-r--r--pages/shop/cart/remove.fnl23
-rw-r--r--pages/shop/index.fnl155
-rw-r--r--pages/shop/order.fnl61
-rw-r--r--pages/shop/success.fnl15
-rw-r--r--pages/staff/before-leaving.fnl36
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 (.. "(&nbsp;" v ") ") "%1&nbsp;")
- (string.gsub (.. "(%s" v ") ") " %1&nbsp;")
- (string.gsub (.. "^(" v ") ") "%1&nbsp;")))))
- result)
-
-(fn test-improve-typography []
- (assert (= (improve-typography "Я лежу на пляжу!")
- "Я&nbsp;лежу на&nbsp;пляжу!"))
- (assert (= (improve-typography "500 рублей мы сняли со счета!")
- "500&nbsp;рублей мы&nbsp;сняли со&nbsp;счета!"))
- (assert (= (improve-typography "500 рублей мы
- сняли со счета!")
- "500&nbsp;рублей мы&nbsp;сняли со&nbsp;счета!")))
(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 "Чайная комната «Белая&nbsp;жаба»"]]
- [: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 "Чайная «Белая&nbsp;жаба»"]]
- [: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}