Initial commit, blog system works
This commit is contained in:
145
.lua/ui.lua
Normal file
145
.lua/ui.lua
Normal file
@@ -0,0 +1,145 @@
|
||||
-- ui.lua — shared HTML component builders.
|
||||
--
|
||||
-- Pure functions: data in, HTML string out. No DB access here; route
|
||||
-- handlers query and pass rows in.
|
||||
|
||||
local web = require "web"
|
||||
|
||||
local M = {}
|
||||
|
||||
-- ── Tag pills ─────────────────────────────────────────────────────────
|
||||
|
||||
-- Render tag pills for a list of tag strings.
|
||||
-- kind scopes the filter link to that section ("blog" filters on the feed at /).
|
||||
-- htmx=true → include hx-get/hx-target/hx-swap for list pages (where #post-list exists)
|
||||
-- htmx=false → plain href links only, for detail pages (no #post-list in DOM)
|
||||
function M.tag_pills(tags_list, kind, htmx)
|
||||
if #tags_list == 0 then return "" end
|
||||
-- Blog tag filters live on the front-page feed; wiki keeps its section page.
|
||||
local base = (kind == "blog") and "/" or ("/" .. kind)
|
||||
local search_path = "/" .. kind .. "/search"
|
||||
local sep = (base == "/") and "?" or "?"
|
||||
local pills = {}
|
||||
for _, tag in ipairs(tags_list) do
|
||||
local pill = '<a class="tag-pill"'
|
||||
.. ' href="' .. base .. sep .. 'tag=' .. EscapePath(tag) .. '"'
|
||||
if htmx then
|
||||
pill = pill
|
||||
.. ' hx-get="' .. search_path .. '?tag=' .. EscapePath(tag) .. '"'
|
||||
.. ' hx-target="#post-list"'
|
||||
.. ' hx-swap="outerHTML"'
|
||||
end
|
||||
pill = pill .. '>' .. EscapeHtml(tag) .. '</a>'
|
||||
pills[#pills+1] = pill
|
||||
end
|
||||
return table.concat(pills, " ")
|
||||
end
|
||||
|
||||
-- Render the tag cloud for a list page (with active-tag state).
|
||||
-- tags_list is the output of db.list_tags(kind).
|
||||
function M.tag_cloud(tags_list, kind, active_tag)
|
||||
if #tags_list == 0 then return "" end
|
||||
local base = (kind == "blog") and "/" or ("/" .. kind)
|
||||
local search_path = "/" .. kind .. "/search"
|
||||
local pills = {}
|
||||
for _, t in ipairs(tags_list) do
|
||||
local is_active = t.tag == active_tag
|
||||
local cls = is_active and "tag-pill tag-pill-active" or "tag-pill"
|
||||
-- Active tag: clicking clears the filter.
|
||||
local qs = "tag=" .. EscapePath(t.tag)
|
||||
local href = is_active and base or (base .. "?" .. qs)
|
||||
local hx_get = is_active and search_path or (search_path .. "?" .. qs)
|
||||
pills[#pills+1] = '<a class="' .. cls .. '"'
|
||||
.. ' href="' .. href .. '"'
|
||||
.. ' hx-get="' .. hx_get .. '"'
|
||||
.. ' hx-target="#post-list"'
|
||||
.. ' hx-swap="outerHTML">'
|
||||
.. EscapeHtml(t.tag) .. '</a>'
|
||||
end
|
||||
return '<div class="tag-cloud">' .. table.concat(pills, "\n") .. '</div>'
|
||||
end
|
||||
|
||||
-- ── Post cards / lists ────────────────────────────────────────────────
|
||||
|
||||
--- Render a single post/wiki card for use in list and search results.
|
||||
function M.post_card(post, kind)
|
||||
local date = web.fmt_date(post.created_at)
|
||||
local summary = (post.summary ~= "" and post.summary)
|
||||
or (post.excerpt and post.excerpt ~= "" and post.excerpt)
|
||||
or post.body:sub(1, 160) .. "…"
|
||||
local tags_list = web.split_tags(post.tags or "")
|
||||
local tags_html = ""
|
||||
if #tags_list > 0 then
|
||||
tags_html = '<div class="post-card-tags">'
|
||||
.. M.tag_pills(tags_list, kind, true) .. '</div>'
|
||||
end
|
||||
local draft_html = post.published == 0
|
||||
and ' <span class="status-badge status-draft">Draft</span>' or ""
|
||||
return '<article class="post-card">'
|
||||
.. '<a href="/' .. kind .. '/' .. EscapePath(post.slug) .. '">'
|
||||
.. '<h2>' .. EscapeHtml(post.title) .. '</h2>'
|
||||
.. '<div class="post-meta"><time datetime="' .. date .. '">' .. date .. '</time>'
|
||||
.. draft_html .. '</div>'
|
||||
.. '<p class="post-excerpt">' .. EscapeHtml(summary) .. '</p>'
|
||||
.. '</a>'
|
||||
.. tags_html
|
||||
.. '</article>'
|
||||
end
|
||||
|
||||
--- Render the swappable list div containing post cards.
|
||||
--- kind sets a CSS modifier class (.post-list-wiki gets a 2-col grid).
|
||||
function M.post_list(posts, kind)
|
||||
if #posts == 0 then
|
||||
return '<div id="post-list" class="post-list post-list-' .. kind
|
||||
.. '"><div class="no-results">No posts found.</div></div>'
|
||||
end
|
||||
local cards = {}
|
||||
for _, post in ipairs(posts) do
|
||||
cards[#cards + 1] = M.post_card(post, kind)
|
||||
end
|
||||
return '<div id="post-list" class="post-list post-list-' .. kind .. '">'
|
||||
.. table.concat(cards, "\n") .. '</div>'
|
||||
end
|
||||
|
||||
--- Render search results fragment (no layout wrapper — HTMX target).
|
||||
--- results: rows from db.search(). label: human-readable query description.
|
||||
function M.search_results(results, kind, label)
|
||||
if #results == 0 then
|
||||
return '<div id="post-list" class="post-list post-list-' .. (kind or "blog")
|
||||
.. '"><div class="no-results">No results for “'
|
||||
.. EscapeHtml(label) .. '”.</div></div>'
|
||||
end
|
||||
local cards = {}
|
||||
for _, post in ipairs(results) do
|
||||
cards[#cards + 1] = M.post_card(post, kind or post.kind)
|
||||
end
|
||||
return '<div id="post-list" class="post-list post-list-' .. (kind or "blog") .. '">'
|
||||
.. table.concat(cards, "\n") .. '</div>'
|
||||
end
|
||||
|
||||
-- ── Pagination ────────────────────────────────────────────────────────
|
||||
|
||||
--- Render older/newer pagination links for the feed.
|
||||
--- base is the page path ("/"); page numbers are 1-based.
|
||||
function M.pagination(base, page, total_pages)
|
||||
if total_pages <= 1 then return "" end
|
||||
local sep = base:find("?", 1, true) and "&" or "?"
|
||||
local function link(p, label, rel)
|
||||
local href = p == 1 and base or (base .. sep .. "page=" .. p)
|
||||
return '<a class="page-link" rel="' .. rel .. '" href="' .. href .. '">'
|
||||
.. label .. '</a>'
|
||||
end
|
||||
local newer = page > 1
|
||||
and link(page - 1, "← Newer", "prev")
|
||||
or '<span class="page-link page-link-disabled">← Newer</span>'
|
||||
local older = page < total_pages
|
||||
and link(page + 1, "Older →", "next")
|
||||
or '<span class="page-link page-link-disabled">Older →</span>'
|
||||
return '<nav class="pagination" aria-label="Pagination">'
|
||||
.. newer
|
||||
.. '<span class="page-current">Page ' .. page .. ' of ' .. total_pages .. '</span>'
|
||||
.. older
|
||||
.. '</nav>'
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user