Monorepo: consolidate 7 repos into one
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m5s

Combines shared, blog, market, cart, events, federation, and account
into a single repository. Eliminates submodule sync, sibling model
copying at build time, and per-app CI orchestration.

Changes:
- Remove per-app .git, .gitmodules, .gitea, submodule shared/ dirs
- Remove stale sibling model copies from each app
- Update all 6 Dockerfiles for monorepo build context (root = .)
- Add build directives to docker-compose.yml
- Add single .gitea/workflows/ci.yml with change detection
- Add .dockerignore for monorepo build context
- Create __init__.py for federation and account (cross-app imports)
This commit is contained in:
giles
2026-02-24 19:44:17 +00:00
commit f42042ccb7
895 changed files with 61147 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
{#
Shared admin navigation macro
Use this instead of duplicate _nav.html files
#}
{% macro admin_nav_item(href, icon='cog', label='', select_colours='', aclass=styles.nav_button) %}
{% import 'macros/links.html' as links %}
{% call links.link(href, hx_select_search, select_colours, True, aclass=aclass) %}
<i class="fa fa-{{ icon }}" aria-hidden="true"></i>
{{ label }}
{% endcall %}
{% endmacro %}
{% macro placeholder_nav() %}
{# Placeholder for admin sections without specific nav items #}
<div class="relative nav-group">
<span class="block px-3 py-2 text-stone-400 text-sm italic">
Admin options
</span>
</div>
{% endmacro %}

View File

@@ -0,0 +1,31 @@
{# Cart icon/badge — shows logo when empty, cart icon with count when items present #}
{% macro cart_icon(count=0, oob=False) %}
<div id="cart-mini" {% if oob %}hx-swap-oob="{{oob}}"{% endif %}>
{% if count == 0 %}
<div class="h-12 w-12 rounded-full overflow-hidden border border-stone-300 flex-shrink-0">
<a
href="{{ blog_url('/') }}"
class="h-full w-full font-bold text-5xl flex-shrink-0 flex flex-row items-center gap-1"
>
<img
src="{{ site().logo }}"
class="h-full w-full rounded-full object-cover border border-stone-300 flex-shrink-0"
>
</a>
</div>
{% else %}
<a
href="{{ cart_url('/') }}"
class="relative inline-flex items-center justify-center text-stone-700 hover:text-emerald-700"
>
<i class="fa fa-shopping-cart text-5xl" aria-hidden="true"></i>
<span
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5"
>
{{ count }}
</span>
</a>
{% endif %}
</div>
{% endmacro %}

View File

@@ -0,0 +1,17 @@
{% macro opener(group=False) %}
<svg
class="h-4 w-4 transition-transform group-open{{ '/' + group if group else ''}}:rotate-180"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 9l6 6 6-6"
/>
</svg>
{% endmacro %}

View File

@@ -0,0 +1,61 @@
{# templates/macros/layout.html #}
{% macro details(group = '', _class='') %}
<details
class="group{{group}} p-2 {{_class}}" data-toggle-group="mobile-panels">
{{ caller() }}
</details>
{%- endmacro %}
{% macro summary(id, _class=None, oob=False) %}
<summary>
<header class="z-50">
<div
id="{{id}}"
{% if oob %}
hx-swap-oob="true"
{% endif %}
class="{{'flex justify-between items-start gap-2' if not _class else _class}}">
{{ caller() }}
</div>
</header>
</summary>
{%- endmacro %}
{% macro filter_summary(id, current_local_href, search, search_count, hx_select, oob=True) %}
<summary class="bg-white/90">
<div class="flex flex-row items-start">
<div>
<div class="md:hidden mx-2 bg-stone-200 rounded">
<span class="flex items-center justify-center text-stone-600 text-lg h-12 w-12 transition-transform group-open/filter:hidden self-start">
<i class="fa-solid fa-filter"></i>
</span>
<span>
<svg aria-hidden="true" viewBox="0 0 24 24"
class="w-12 h-12 rotate-180 transition-transform group-open/filter:block hidden self-start">
<path d="M6 9l6 6 6-6" fill="currentColor"/>
</svg>
</span>
</div>
</div>
<div
id="{{id}}"
class="flex-1 md:hidden grid grid-cols-12 items-center gap-3"
>
<div class="flex flex-col items-start gap-2">
{{ caller() }}
</div>
</div>
{% from 'macros/search.html' import search_mobile %}
{{ search_mobile(current_local_href, search, search_count, hx_select) }}
</div>
</summary>
{%- endmacro %}
{% macro menu(id, _class="") %}
<div id="{{id}}" hx-swap-oob="outerHTML" class="{{_class}}">
{{ caller() }}
</div>
{%- endmacro %}

View File

@@ -0,0 +1,59 @@
{% macro link(url, select, select_colours='', highlight=True, _class='', aclass='') %}
{% set href=url|host%}
<div class="relative nav-group {{_class}}">
<a
href="{{ href }}"
hx-get="{{ href }}"
hx-target="#main-panel"
hx-select="{{select}}"
hx-swap="outerHTML"
hx-push-url="true"
aria-selected="{{ 'true' if (request.path|host).startswith(href) else 'false' }}"
{% if aclass %}
class="{{aclass}}"
{% elif select_colours %}
class="whitespace-normal flex gap-2 px-3 py-2 rounded
text-center break-words leading-snug
bg-stone-200 text-black
{{select_colours if highlight else ''}}
"
{% else %}
class="w-full whitespace-normal flex items-center gap-2 font-bold text-2xl px-3 py-2"
{% endif %}
>
{{ caller() }}
</a>
</div>
{% endmacro %}
{% macro menu_row(id=False, oob=False) %}
<div
{% if id %}
id="{{id}}"
{% endif %}
{% if oob %}
hx-swap-oob="outerHTML"
{% endif %}
class="flex flex-col items-center md:flex-row justify-center md:justify-between w-full p-1 bg-{{menu_colour}}-{{(500-(level()*100))|string}}"
>
{{ caller() }}
</div>
{{level_up()}}
{% endmacro %}
{% macro desktop_nav() %}
<nav class="hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0">
{{ caller() }}
</nav>
{% endmacro %}
{% macro admin() %}
<i class="fa fa-cog" aria-hidden="true"></i>
<div>
settings
</div>
{% endmacro %}

View File

@@ -0,0 +1,68 @@
{#
Scrolling menu macro with arrow navigation
Creates a horizontally scrollable menu (desktop) or vertically scrollable (mobile)
with arrow buttons that appear/hide based on content overflow.
Parameters:
- container_id: Unique ID for the scroll container
- items: List of items to iterate over
- item_content: Caller block that renders each item (receives 'item' variable)
- wrapper_class: Optional additional classes for outer wrapper
- container_class: Optional additional classes for scroll container
- item_class: Optional additional classes for each item wrapper
#}
{% macro scrolling_menu(container_id, items, wrapper_class='', container_class='', item_class='') %}
{% if items %}
{# Left scroll arrow - desktop only #}
<button
class="scrolling-menu-arrow-{{ container_id }} hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
aria-label="Scroll left"
_="on click
set #{{ container_id }}.scrollLeft to #{{ container_id }}.scrollLeft - 200">
<i class="fa fa-chevron-left"></i>
</button>
{# Scrollable container #}
<div id="{{ container_id }}"
class="overflow-y-auto sm:overflow-x-auto sm:overflow-y-visible scrollbar-hide max-h-[50vh] sm:max-h-none {{ container_class }}"
style="scroll-behavior: smooth;"
_="on load or scroll
-- Show arrows if content overflows (desktop only)
if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth
remove .hidden from .scrolling-menu-arrow-{{ container_id }}
add .flex to .scrolling-menu-arrow-{{ container_id }}
else
add .hidden to .scrolling-menu-arrow-{{ container_id }}
remove .flex from .scrolling-menu-arrow-{{ container_id }}
end">
<div class="flex flex-col sm:flex-row gap-1 {{ wrapper_class }}">
{% for item in items %}
<div class="{{ item_class }}">
{{ caller(item) }}
</div>
{% endfor %}
</div>
</div>
<style>
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
{# Right scroll arrow - desktop only #}
<button
class="scrolling-menu-arrow-{{ container_id }} hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
aria-label="Scroll right"
_="on click
set #{{ container_id }}.scrollLeft to #{{ container_id }}.scrollLeft + 200">
<i class="fa fa-chevron-right"></i>
</button>
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,83 @@
{# Shared search input macros for filter UIs #}
{% macro search_mobile(current_local_href, search, search_count, hx_select) -%}
<div
id="search-mobile-wrapper"
class="flex flex-row gap-2 items-center flex-1 min-w-0 pr-2"
>
<input
id="search-mobile"
type="text"
name="search"
aria-label="search"
class="text-base md:text-sm col-span-5 rounded-md px-3 py-2 mb-2 w-full min-w-0 max-w-full border-2 border-stone-200 placeholder-shown:border-stone-200 [&:not(:placeholder-shown)]:border-yellow-200"
hx-preserve
value="{{ search|default('', true) }}"
placeholder="search"
hx-trigger="input changed delay:300ms"
hx-target="#main-panel"
hx-select="{{hx_select}}, #search-mobile-wrapper, #search-desktop-wrapper"
hx-get="{{ (current_local_href ~ {'search': None}|qs)|host }}"
hx-swap="outerHTML"
hx-push-url="true"
hx-headers='{"X-Origin":"search-mobile", "X-Search":"true"}'
hx-sync="this:replace"
autocomplete="off"
>
<div
id="search-count-mobile"
aria-label="search count"
{% if not search_count %}
class="text-xl text-red-500"
{% endif %}
>
{% if search %}
{{search_count}}
{% endif %}
</div>
</div>
{%- endmacro %}
{% macro search_desktop(current_local_href, search, search_count, hx_select) -%}
<div
id="search-desktop-wrapper"
class="flex flex-row gap-2 items-center"
>
<input
id="search-desktop"
type="text"
name="search"
aria-label="search"
class="w-full mx-1 my-3 px-3 py-2 text-md rounded-xl border-2 shadow-sm border-white placeholder-shown:border-white [&:not(:placeholder-shown)]:border-yellow-200"
hx-preserve
value="{{ search|default('', true) }}"
placeholder="search"
hx-trigger="input changed delay:300ms"
hx-target="#main-panel"
hx-select="{{hx_select}}, #search-mobile-wrapper, #search-desktop-wrapper"
hx-get="{{ (current_local_href ~ {'search': None}|qs)|host}}"
hx-swap="outerHTML"
hx-push-url="true"
hx-headers='{"X-Origin":"search-desktop", "X-Search":"true"}'
hx-sync="this:replace"
autocomplete="off"
>
<div
id="search-count-desktop"
aria-label="search count"
{% if not search_count %}
class="text-xl text-red-500"
{% endif %}
>
{% if search %}
{{search_count}}
{% endif %}
{{zap_filter}}
</div>
</div>
{%- endmacro %}

View File

@@ -0,0 +1,24 @@
{% macro sticker(src, title, enabled, size=40, found=false) -%}
<span class="relative inline-flex items-center justify-center group"
tabindex="0" aria-label="{{ title|capitalize }}">
<!-- sticker icon -->
<img
src="{{ src }}"
width="{{size}}" height="{{size}}"
alt="{{ title|capitalize }}"
title="{{ title|capitalize }}"
class="{% if found %}border-2 border-yellow-200 bg-yellow-300{% endif %} {%if enabled %} opacity-100 {% else %} opacity-40 saturate-0 {% endif %}"
/>
<!-- tooltip -->
<span role="tooltip"
class="pointer-events-none absolute z-50 bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover/tt:block group-focus-visible/tt:block whitespace-nowrap rounded-md bg-stone-900 text-white text-xs px-2 py-1 shadow-lg">
{{ title|capitalize if title|lower != 'sugarfree' else 'Sugar' }}
<!-- little arrow -->
<span class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-stone-900"></span>
</span>
</span>
{%- endmacro -%}

View File

@@ -0,0 +1,10 @@
{% macro title(_class='') %}
<a
href="{{ blog_url('/') }}"
class="{{_class}}"
>
<h1>
{{ site().title }}
</h1>
</a>
{% endmacro %}