(import-macros {: map : reduce} :lib.macro)
(tset package :path (.. package.path ";./vendor/lpeglj/?.lua"))
(local io (require :io))
(local math (require :math))
(local fennel (require :vendor.fennel))
(local html (require :vendor.html))
(local json (require :vendor.json))
(local array (require :lib.array))
(local str (require :lib.string))
(local ozchai (require :parser.ozchai))
(local ipuer (require :parser.ipuer))
(local artoftea (require :parser.artoftea))
(when _G.unpack
(tset table :unpack _G.unpack))
(local db (luna.db.open "file:var/db.sqlite?_journal=WAL&_sync=NORMAL"))
(local query-synonyms {
"шэн" "шен"
"шен" "шэн"
"доска" "чабань"
"чабань" "доска"})
(fn unescape [s]
(assert (= (type s) :string) "s must be string")
(pick-values 1
(-> s
(string.gsub "<" "<")
(string.gsub ">" ">")
(string.gsub """ "\"")
(string.gsub "&" "&"))))
(fn site-name-template [name]
(if
(= name "ipuer")
[:a {:class "site-icon" :href "https://ipuer.ru"
:alt "Логотип Институт чай пуэр"}
[:img {:src "/static/ipuer.jpg"}]
"Институт чая пуэр"]
(= name "artoftea")
[:a {:class "site-icon" :href "https://artoftea.ru"
:alt "Логотип Art of tea"}
[:img {:src "/static/artoftea.png"}]
"Art of tea"]
(= name "ozchai")
[:a {:class "site-icon" :href "https://ozchai.ru"
:alt "Логотип #OZCHAI"}
[:img {:src "/static/ozchai.ico"}]
"Чайная #OZCHAI"]
(= name "clubcha")
[:a {:class "site-icon" :href "https://clubcha.ru"
:alt "Логотип Железный Феникс"}
[:img {:src "/static/clubcha.png"}]
"Железный Феникс"]
""))
(fn item-template [product]
[:div {:class "tile"}
[:a {:href product.url :style "display: block;"}
[:img {:src product.image} ""]]
(site-name-template product.site)
[:a {:href product.url :style "text-decoration: none;"}
[:NO-ESCAPE (.. "
" (unescape product.title) "
")]]
[:div {:class "price"}
(if product.price (.. product.price "₽") "")
(if product.quantity (.. " за " product.quantity "г") "")
(if (and product.price-per
(< 0 product.price-per))
[:NO-ESCAPE (.. " (" product.price-per "₽ за 1г)")]
"")]
[:small {} (or product.description "")]])
(fn paginator-template [query page limit total]
(local last-page (math.ceil (/ total limit)))
(if (< limit total)
[:div {:class "paginator"}
[:div {:class "paginator-numbers"}
(if (< 1 page)
[:a {:href (.. "?page=" (- page 1) "&query=" query)} "<"]
"")
(faccumulate [res [:span {}] i 1 last-page]
(do
(table.insert
res [:a {:href (.. "?page=" i "&query=" query)
:class (if (= page i) "paginator-active" "")}
(tostring i)])
res))
(if (< page last-page)
[:a {:href (.. "?page=" (+ page 1) "&query=" query)} ">"]
"")]
[:div {} "Всего результатов: " [:strong {} (string.format "%d" total)]]]
""))
(fn base-template [query sort page total ...]
(local paginator (paginator-template query page 48 total))
[:html {:lang "en"}
[:head {}
[:meta {:charset "UTF-8"}]
[:link {:rel :stylesheet :href "static/style.css"}]
[:title {} "A new cool web server for lua"]]
[:body {}
[:div {:class "container"}
[:div {:class "content"}
[:aside {:class "aside"}
[:div {:class "aside-content"}
[:a {:href "/" :style "display: block;"}
[:img {:class "logo" :src "static/logo.svg" :alt "Логотип meicha.ru"}]]
[:form {:class "form"}
[:input {:type :search :name :query :value query
:autofocus true :placeholder "enter search query"}]
[:button {:type :submit} "Искать"]]
paginator]]
[:section {}
[:div {:class "list"} ...]
[:footer {} paginator]]]]]])
(fn random-products [limit]
(assert (< 0 limit) "limit must be > 0")
(luna.db.query*
db
"SELECT id,
site,
title,
description,
image,
url,
price,
weight,
price_per AS \"price-per\",
year
FROM products
ORDER BY RANDOM()
LIMIT ?"
[limit]))
(fn query-products [page query sorters]
(local query
(table.concat
(map (fn [_ q]
(if (. query-synonyms q)
(.. "(" q "* OR " (. query-synonyms q) "*)")
(.. q "*")))
(str.split query))
" "))
(local total
(luna.db.query
db
"SELECT count(*)
FROM search
WHERE search.`table` = 'products'
AND search.name MATCH ?"
[query]))
{:results
(luna.db.query*
db
"SELECT products.id,
highlight(search, 0, '', '') AS \"title\",
products.site,
products.description,
products.image,
products.url,
products.price,
products.weight,
products.price_per AS \"price-per\",
products.year
FROM search
INNER JOIN products ON search.fid = products.id
WHERE search.`table` = 'products'
AND search.name MATCH ?
ORDER BY rank
LIMIT 48 OFFSET ?"
[query (* (- page 1) 48)])
:total (if (< 0 (# total))
(. total 1 1)
0)})
(fn root-handler [{: path : query}]
(if (= path "/")
(let [headers {:content-type "text/html"}
page (or (tonumber query.page) 1)
search (or query.query "")
sort "ASC"
{: results : total}
(if (~= "" search)
(query-products page search sort)
{:total 48 :results (random-products 48)})]
(values
200 headers
(html.render
(base-template
search sort page total
(table.unpack (map #(item-template $2) results)))
true)))
(values 404 {} "not found")))
(luna.router.route "GET /" root-handler)
(luna.router.static "GET /static/" "static/")