brewlog/templates/base.html
Jon Seager 0fd4214b6c
refactor(static): prefix all static asset routes with /static/
Move static asset URLs from root-level paths to /static/ prefix
mirroring the filesystem layout (e.g. /styles.css → /static/css/styles.css,
/components/*.js → /static/js/components/*.js).
2026-02-08 18:07:10 +00:00

155 lines
6.5 KiB
HTML

<!doctype html>
<html lang="en" data-star-root>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="{% block description %}Self-hosted coffee logging — track roasters, roasts, bags, brews, and gear.{% endblock %}"
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Brewlog" />
<meta property="og:title" content="{% block og_title %}Brewlog{% endblock %}" />
<meta property="og:description" content="{% block og_description %}Self-hosted coffee logging — track roasters, roasts, bags, brews, and gear.{% endblock %}" />
<meta name="twitter:card" content="summary_large_image" />
<title>{% block title %}Brewlog{% endblock %}</title>
<link rel="stylesheet" href="/static/css/styles.css" />
<link rel="icon" id="favicon" type="image/svg+xml" href="/static/favicon-light.svg" />
<script>
;(() => {
const stored = localStorage.getItem("theme")
const isDark =
stored === "dark" || (!stored && matchMedia("(prefers-color-scheme: dark)").matches)
if (isDark) {
document.documentElement.setAttribute("data-theme", "dark")
document.getElementById("favicon").href = "/static/favicon-dark.svg"
}
})()
</script>
<script
type="module"
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"
></script>
<script defer src="/static/js/components/photo-capture.js"></script>
<script defer src="/static/js/components/searchable-select.js"></script>
<script defer src="/static/js/components/chip-scroll.js"></script>
<script defer src="/static/js/components/world-map.js"></script>
<script defer src="/static/js/components/donut-chart.js"></script>
{% block head %}{% endblock %}
<script>
const toggleRow = (event) => {
if (event.target.closest(".card-detail")) return
const row = event.currentTarget
row.classList.toggle("expanded")
row.querySelector(".card-detail")?.classList.toggle("hidden")
const r = row.nextElementSibling
if (r?.classList.contains("detail-row")) r.classList.toggle("hidden")
row.querySelector(".icon-expand")?.classList.toggle("hidden")
row.querySelector(".icon-collapse")?.classList.toggle("hidden")
}
</script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const mobileQuery = window.matchMedia("(max-width: 767px)")
if (!mobileQuery.matches) return
const setupInfiniteScroll = () => {
document.querySelectorAll("[data-infinite-scroll]").forEach((container) => {
const sentinel = container.querySelector(".infinite-scroll-sentinel")
if (!sentinel || sentinel.dataset.observed) return
sentinel.dataset.observed = "true"
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return
const nextUrl = container.getAttribute("data-next-url")
const targetSelector = container.getAttribute("data-target")
if (!nextUrl || !targetSelector) {
observer.disconnect()
return
}
observer.disconnect()
sentinel.remove()
fetch(nextUrl, {
headers: { "datastar-request": "true" },
})
.then((res) => res.text())
.then((html) => {
const doc = new DOMParser().parseFromString(html, "text/html")
const newTarget = doc.querySelector(targetSelector)
if (!newTarget) return
// Append new rows to existing tbody
const existingTbody = container.querySelector("tbody")
const newTbody = newTarget.querySelector("tbody")
if (existingTbody && newTbody) {
Array.from(newTbody.children).forEach((row) => {
existingTbody.appendChild(document.adoptNode(row))
})
}
// Check if there are more pages
const newContainer = newTarget.querySelector("[data-infinite-scroll]")
const newNextUrl = newContainer && newContainer.getAttribute("data-next-url")
if (newNextUrl) {
container.setAttribute("data-next-url", newNextUrl)
const newSentinel = document.createElement("div")
newSentinel.className = "infinite-scroll-sentinel h-4 md:hidden"
newSentinel.setAttribute("aria-hidden", "true")
container.appendChild(newSentinel)
setupInfiniteScroll()
} else {
container.removeAttribute("data-next-url")
container.removeAttribute("data-infinite-scroll")
}
})
})
},
{ rootMargin: "200px" }
)
observer.observe(sentinel)
})
}
setupInfiniteScroll()
const bodyObserver = new MutationObserver(() => {
setupInfiniteScroll()
})
bodyObserver.observe(document.body, { childList: true, subtree: true })
})
</script>
</head>
<body class="min-h-screen bg-page text-text">
<main class="mx-auto flex w-full max-w-5xl flex-col gap-8 px-6 py-4 md:py-10">
{% include "partials/nav.html" %} {% block content %}{% endblock %}
<footer
class="mt-8 text-center text-xs tracking-widest text-text-muted small-caps"
>
<p>
B{rew}log by
<a
href="https://jnsgr.uk"
class="text-text-muted hover:text-accent transition"
target="_blank"
rel="noreferrer noopener"
>jnsgruk</a
>
·
<a
href="https://github.com/jnsgruk/brewlog/commit/{{ version_info.commit }}"
class="text-text-muted hover:text-accent transition"
target="_blank"
rel="noreferrer noopener"
>{{ version_info.commit }}</a
>
</p>
</footer>
</main>
</body>
</html>