-- 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 = '' .. EscapeHtml(tag) .. '' 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] = '' .. EscapeHtml(t.tag) .. '' end return '
' .. table.concat(pills, "\n") .. '
' 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 = '
' .. M.tag_pills(tags_list, kind, true) .. '
' end local draft_html = post.published == 0 and ' Draft' or "" return '
' .. '' .. '

' .. EscapeHtml(post.title) .. '

' .. '' .. '

' .. EscapeHtml(summary) .. '

' .. '
' .. tags_html .. '
' 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 '
No posts found.
' end local cards = {} for _, post in ipairs(posts) do cards[#cards + 1] = M.post_card(post, kind) end return '
' .. table.concat(cards, "\n") .. '
' 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 '
No results for “' .. EscapeHtml(label) .. '”.
' end local cards = {} for _, post in ipairs(results) do cards[#cards + 1] = M.post_card(post, kind or post.kind) end return '
' .. table.concat(cards, "\n") .. '
' 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 '' .. label .. '' end local newer = page > 1 and link(page - 1, "← Newer", "prev") or '← Newer' local older = page < total_pages and link(page + 1, "Older →", "next") or 'Older →' return '' end return M