feat: add markets and payments management pages
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 45s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 45s
- New markets blueprint at /<slug>/markets/ with create/delete - New payments blueprint at /<slug>/payments/ with SumUp config - Register both in events app with context processor for markets - Remove PageConfig feature flag check from calendar creation (feature toggles replaced by direct management pages) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
23
app.py
23
app.py
@@ -9,7 +9,7 @@ from sqlalchemy import select
|
||||
|
||||
from shared.factory import create_base_app
|
||||
|
||||
from suma_browser.app.bp import register_calendars
|
||||
from suma_browser.app.bp import register_calendars, register_markets, register_payments
|
||||
|
||||
|
||||
async def events_context() -> dict:
|
||||
@@ -43,6 +43,7 @@ async def events_context() -> dict:
|
||||
def create_app() -> "Quart":
|
||||
from models.ghost_content import Post
|
||||
from models.calendars import Calendar
|
||||
from models.market_place import MarketPlace
|
||||
|
||||
app = create_base_app("events", context_fn=events_context)
|
||||
|
||||
@@ -59,6 +60,18 @@ def create_app() -> "Quart":
|
||||
url_prefix="/<slug>/calendars",
|
||||
)
|
||||
|
||||
# Markets nested under post slug: /<slug>/markets/...
|
||||
app.register_blueprint(
|
||||
register_markets(),
|
||||
url_prefix="/<slug>/markets",
|
||||
)
|
||||
|
||||
# Payments nested under post slug: /<slug>/payments/...
|
||||
app.register_blueprint(
|
||||
register_payments(),
|
||||
url_prefix="/<slug>/payments",
|
||||
)
|
||||
|
||||
# --- Auto-inject slug into url_for() calls ---
|
||||
@app.url_value_preprocessor
|
||||
def pull_slug(endpoint, values):
|
||||
@@ -109,9 +122,17 @@ def create_app() -> "Quart":
|
||||
.order_by(Calendar.name.asc())
|
||||
)
|
||||
).scalars().all()
|
||||
markets = (
|
||||
await g.s.execute(
|
||||
select(MarketPlace)
|
||||
.where(MarketPlace.post_id == post_id, MarketPlace.deleted_at.is_(None))
|
||||
.order_by(MarketPlace.name.asc())
|
||||
)
|
||||
).scalars().all()
|
||||
return {
|
||||
**post_data,
|
||||
"calendars": calendars,
|
||||
"markets": markets,
|
||||
}
|
||||
|
||||
# Tickets blueprint — user-facing ticket views and QR codes
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
from .calendars.routes import register as register_calendars
|
||||
from .markets.routes import register as register_markets
|
||||
from .payments.routes import register as register_payments
|
||||
|
||||
@@ -5,7 +5,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.calendars import Calendar
|
||||
from models.ghost_content import Post # for FK existence checks
|
||||
from models.page_config import PageConfig
|
||||
import unicodedata
|
||||
import re
|
||||
|
||||
@@ -88,12 +87,6 @@ async def create_calendar(sess: AsyncSession, post_id: int, name: str) -> Calend
|
||||
if not post.is_page:
|
||||
raise CalendarError("Calendars can only be created on pages, not posts.")
|
||||
|
||||
pc = (await sess.execute(
|
||||
select(PageConfig).where(PageConfig.post_id == post_id)
|
||||
)).scalar_one_or_none()
|
||||
if pc is None or not (pc.features or {}).get("calendar"):
|
||||
raise CalendarError("Calendar feature is not enabled for this page. Enable it in page settings first.")
|
||||
|
||||
# Look for existing (including soft-deleted)
|
||||
q = await sess.execute(
|
||||
select(Calendar).where(Calendar.post_id == post_id, Calendar.name == name)
|
||||
|
||||
0
bp/markets/__init__.py
Normal file
0
bp/markets/__init__.py
Normal file
68
bp/markets/routes.py
Normal file
68
bp/markets/routes.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from quart import (
|
||||
request, render_template, make_response, Blueprint, g
|
||||
)
|
||||
from sqlalchemy import select
|
||||
|
||||
from models.market_place import MarketPlace
|
||||
|
||||
from .services.markets import (
|
||||
create_market as svc_create_market,
|
||||
soft_delete as svc_soft_delete,
|
||||
)
|
||||
|
||||
from suma_browser.app.redis_cacher import cache_page, clear_cache
|
||||
from suma_browser.app.authz import require_admin
|
||||
from suma_browser.app.utils.htmx import is_htmx_request
|
||||
|
||||
|
||||
def register():
|
||||
bp = Blueprint("markets", __name__, url_prefix='/markets')
|
||||
|
||||
@bp.context_processor
|
||||
async def inject_root():
|
||||
return {}
|
||||
|
||||
@bp.get("/")
|
||||
async def home(**kwargs):
|
||||
if not is_htmx_request():
|
||||
html = await render_template("_types/markets/index.html")
|
||||
else:
|
||||
html = await render_template("_types/markets/_oob_elements.html")
|
||||
return await make_response(html)
|
||||
|
||||
@bp.post("/new/")
|
||||
@require_admin
|
||||
async def create_market(**kwargs):
|
||||
form = await request.form
|
||||
name = (form.get("name") or "").strip()
|
||||
|
||||
post_data = getattr(g, "post_data", None)
|
||||
post_id = (post_data.get("post") or {}).get("id") if post_data else None
|
||||
|
||||
if not post_id:
|
||||
post_id = form.get("post_id")
|
||||
if post_id:
|
||||
post_id = int(post_id)
|
||||
|
||||
try:
|
||||
await svc_create_market(g.s, post_id, name)
|
||||
except Exception as e:
|
||||
return await make_response(f'<div class="text-red-600 text-sm">{e}</div>', 422)
|
||||
|
||||
html = await render_template("_types/markets/index.html")
|
||||
return await make_response(html)
|
||||
|
||||
@bp.delete("/<market_slug>/")
|
||||
@require_admin
|
||||
async def delete_market(market_slug: str, **kwargs):
|
||||
post_slug = getattr(g, "post_slug", None)
|
||||
deleted = await svc_soft_delete(g.s, post_slug, market_slug)
|
||||
if not deleted:
|
||||
return await make_response("Market not found", 404)
|
||||
|
||||
html = await render_template("_types/markets/index.html")
|
||||
return await make_response(html)
|
||||
|
||||
return bp
|
||||
0
bp/markets/services/__init__.py
Normal file
0
bp/markets/services/__init__.py
Normal file
85
bp/markets/services/markets.py
Normal file
85
bp/markets/services/markets.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.market_place import MarketPlace
|
||||
from models.ghost_content import Post
|
||||
from suma_browser.app.utils import utcnow
|
||||
|
||||
|
||||
class MarketError(ValueError):
|
||||
"""Base error for market service operations."""
|
||||
|
||||
|
||||
def slugify(value: str, max_len: int = 255) -> str:
|
||||
if value is None:
|
||||
value = ""
|
||||
value = unicodedata.normalize("NFKD", value)
|
||||
value = value.encode("ascii", "ignore").decode("ascii")
|
||||
value = value.lower()
|
||||
value = value.replace("/", "-")
|
||||
value = re.sub(r"[^a-z0-9]+", "-", value)
|
||||
value = re.sub(r"-{2,}", "-", value)
|
||||
value = value.strip("-")[:max_len].strip("-")
|
||||
return value or "market"
|
||||
|
||||
|
||||
async def create_market(sess: AsyncSession, post_id: int, name: str) -> MarketPlace:
|
||||
"""
|
||||
Create a market for a page. Name must be unique per page.
|
||||
If a market with the same (post_id, slug) exists but is soft-deleted,
|
||||
it will be revived.
|
||||
"""
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
raise MarketError("Market name must not be empty.")
|
||||
slug = slugify(name)
|
||||
|
||||
post = (await sess.execute(select(Post).where(Post.id == post_id))).scalar_one_or_none()
|
||||
if not post:
|
||||
raise MarketError(f"Post {post_id} does not exist.")
|
||||
if not post.is_page:
|
||||
raise MarketError("Markets can only be created on pages, not posts.")
|
||||
|
||||
# Look for existing (including soft-deleted)
|
||||
existing = (await sess.execute(
|
||||
select(MarketPlace).where(MarketPlace.post_id == post_id, MarketPlace.slug == slug)
|
||||
)).scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
if existing.deleted_at is not None:
|
||||
existing.deleted_at = None
|
||||
existing.name = name
|
||||
await sess.flush()
|
||||
return existing
|
||||
raise MarketError(f'Market with slug "{slug}" already exists for this page.')
|
||||
|
||||
market = MarketPlace(post_id=post_id, name=name, slug=slug)
|
||||
sess.add(market)
|
||||
await sess.flush()
|
||||
return market
|
||||
|
||||
|
||||
async def soft_delete(sess: AsyncSession, post_slug: str, market_slug: str) -> bool:
|
||||
market = (
|
||||
await sess.execute(
|
||||
select(MarketPlace)
|
||||
.join(Post, MarketPlace.post_id == Post.id)
|
||||
.where(
|
||||
Post.slug == post_slug,
|
||||
MarketPlace.slug == market_slug,
|
||||
MarketPlace.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if not market:
|
||||
return False
|
||||
|
||||
market.deleted_at = utcnow()
|
||||
await sess.flush()
|
||||
return True
|
||||
0
bp/payments/__init__.py
Normal file
0
bp/payments/__init__.py
Normal file
81
bp/payments/routes.py
Normal file
81
bp/payments/routes.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from quart import (
|
||||
render_template, make_response, Blueprint, g, request
|
||||
)
|
||||
from sqlalchemy import select
|
||||
|
||||
from models.page_config import PageConfig
|
||||
|
||||
from suma_browser.app.authz import require_admin
|
||||
from suma_browser.app.utils.htmx import is_htmx_request
|
||||
|
||||
|
||||
def register():
|
||||
bp = Blueprint("payments", __name__, url_prefix='/payments')
|
||||
|
||||
@bp.context_processor
|
||||
async def inject_root():
|
||||
return {}
|
||||
|
||||
async def _load_payment_ctx():
|
||||
"""Load PageConfig SumUp data for the current page."""
|
||||
post = (getattr(g, "post_data", None) or {}).get("post", {})
|
||||
post_id = post.get("id")
|
||||
if not post_id:
|
||||
return {}
|
||||
|
||||
pc = (await g.s.execute(
|
||||
select(PageConfig).where(PageConfig.post_id == post_id)
|
||||
)).scalar_one_or_none()
|
||||
|
||||
return {
|
||||
"sumup_configured": bool(pc and pc.sumup_api_key),
|
||||
"sumup_merchant_code": (pc.sumup_merchant_code or "") if pc else "",
|
||||
"sumup_checkout_prefix": (pc.sumup_checkout_prefix or "") if pc else "",
|
||||
}
|
||||
|
||||
@bp.get("/")
|
||||
@require_admin
|
||||
async def home(**kwargs):
|
||||
ctx = await _load_payment_ctx()
|
||||
if not is_htmx_request():
|
||||
html = await render_template("_types/payments/index.html", **ctx)
|
||||
else:
|
||||
html = await render_template("_types/payments/_oob_elements.html", **ctx)
|
||||
return await make_response(html)
|
||||
|
||||
@bp.put("/")
|
||||
@require_admin
|
||||
async def update_sumup(**kwargs):
|
||||
"""Update SumUp credentials for this page."""
|
||||
post = (getattr(g, "post_data", None) or {}).get("post", {})
|
||||
post_id = post.get("id")
|
||||
if not post_id:
|
||||
return await make_response("Post not found", 404)
|
||||
|
||||
pc = (await g.s.execute(
|
||||
select(PageConfig).where(PageConfig.post_id == post_id)
|
||||
)).scalar_one_or_none()
|
||||
if pc is None:
|
||||
pc = PageConfig(post_id=post_id, features={})
|
||||
g.s.add(pc)
|
||||
await g.s.flush()
|
||||
|
||||
form = await request.form
|
||||
merchant_code = (form.get("merchant_code") or "").strip()
|
||||
api_key = (form.get("api_key") or "").strip()
|
||||
checkout_prefix = (form.get("checkout_prefix") or "").strip()
|
||||
|
||||
pc.sumup_merchant_code = merchant_code or None
|
||||
pc.sumup_checkout_prefix = checkout_prefix or None
|
||||
if api_key:
|
||||
pc.sumup_api_key = api_key
|
||||
|
||||
await g.s.flush()
|
||||
|
||||
ctx = await _load_payment_ctx()
|
||||
html = await render_template("_types/payments/_main_panel.html", **ctx)
|
||||
return await make_response(html)
|
||||
|
||||
return bp
|
||||
25
templates/_types/markets/_main_panel.html
Normal file
25
templates/_types/markets/_main_panel.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<section class="p-4">
|
||||
{% if has_access('markets.create_market') %}
|
||||
<div id="market-create-errors" class="mt-2 text-sm text-red-600"></div>
|
||||
|
||||
<form
|
||||
class="mt-4 flex gap-2 items-end"
|
||||
hx-post="{{ url_for('markets.create_market') }}"
|
||||
hx-target="#markets-list"
|
||||
hx-select="#markets-list"
|
||||
hx-swap="outerHTML"
|
||||
hx-on::before-request="document.querySelector('#market-create-errors').textContent='';"
|
||||
hx-on::response-error="document.querySelector('#market-create-errors').innerHTML = event.detail.xhr.responseText;"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm text-gray-600">Name</label>
|
||||
<input name="name" type="text" required class="w-full border rounded px-3 py-2" placeholder="e.g. Farm Shop, Bakery" />
|
||||
</div>
|
||||
<button type="submit" class="border rounded px-3 py-2">Add market</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div id="markets-list" class="mt-6">
|
||||
{% include "_types/markets/_markets_list.html" %}
|
||||
</div>
|
||||
</section>
|
||||
37
templates/_types/markets/_markets_list.html
Normal file
37
templates/_types/markets/_markets_list.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% for m in markets %}
|
||||
<div class="mt-6 border rounded-lg p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
|
||||
{% set market_href = market_url('/' + post.slug + '/' + m.slug + '/') %}
|
||||
<a
|
||||
class="flex items-baseline gap-3"
|
||||
href="{{ market_href }}"
|
||||
>
|
||||
<h3 class="font-semibold">{{ m.name }}</h3>
|
||||
<h4 class="text-gray-500">/{{ m.slug }}/</h4>
|
||||
</a>
|
||||
|
||||
<button
|
||||
class="text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"
|
||||
data-confirm
|
||||
data-confirm-title="Delete market?"
|
||||
data-confirm-text="Products will be hidden (soft delete)"
|
||||
data-confirm-icon="warning"
|
||||
data-confirm-confirm-text="Yes, delete it"
|
||||
data-confirm-cancel-text="Cancel"
|
||||
data-confirm-event="confirmed"
|
||||
hx-delete="{{ url_for('markets.delete_market', market_slug=m.slug) }}"
|
||||
hx-trigger="confirmed"
|
||||
hx-target="#markets-list"
|
||||
hx-select="#markets-list"
|
||||
hx-swap="outerHTML"
|
||||
hx-headers='{"X-CSRFToken":"{{ csrf_token() }}"}'
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500 mt-4">No markets yet. Create one above.</p>
|
||||
{% endfor %}
|
||||
2
templates/_types/markets/_nav.html
Normal file
2
templates/_types/markets/_nav.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% from 'macros/admin_nav.html' import placeholder_nav %}
|
||||
{{ placeholder_nav() }}
|
||||
19
templates/_types/markets/_oob_elements.html
Normal file
19
templates/_types/markets/_oob_elements.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends 'oob_elements.html' %}
|
||||
|
||||
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
|
||||
|
||||
{% block oobs %}
|
||||
{% from '_types/root/_n/macros.html' import oob_header with context %}
|
||||
{{oob_header('post-admin-header-child', 'markets-header-child', '_types/markets/header/_header.html')}}
|
||||
|
||||
{% from '_types/post/admin/header/_header.html' import header_row with context %}
|
||||
{{ header_row(oob=True) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block mobile_menu %}
|
||||
{% include '_types/markets/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_types/markets/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
14
templates/_types/markets/header/_header.html
Normal file
14
templates/_types/markets/header/_header.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='markets-row', oob=oob) %}
|
||||
{% call links.link(url_for('markets.home'), hx_select_search) %}
|
||||
<i class="fa fa-shopping-bag" aria-hidden="true"></i>
|
||||
<div>
|
||||
Markets
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% include '_types/markets/_nav.html' %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
21
templates/_types/markets/index.html
Normal file
21
templates/_types/markets/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends '_types/root/_index.html' %}
|
||||
|
||||
{% block meta %}{% endblock %}
|
||||
|
||||
{% block root_header_child %}
|
||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||
{% call index_row('post-header-child', '_types/post/header/_header.html') %}
|
||||
{% call index_row('markets-header-child', '_types/markets/header/_header.html') %}
|
||||
{% block markets_header_child %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
{% block _main_mobile_menu %}
|
||||
{% include '_types/markets/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/markets/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
70
templates/_types/payments/_main_panel.html
Normal file
70
templates/_types/payments/_main_panel.html
Normal file
@@ -0,0 +1,70 @@
|
||||
<section class="p-4 max-w-lg mx-auto">
|
||||
<div id="payments-panel" class="space-y-4 p-4 bg-white rounded-lg border border-stone-200">
|
||||
<h3 class="text-lg font-semibold text-stone-800">
|
||||
<i class="fa fa-credit-card text-purple-600 mr-1"></i>
|
||||
SumUp Payment
|
||||
</h3>
|
||||
<p class="text-xs text-stone-400">
|
||||
Configure per-page SumUp credentials. Leave blank to use the global merchant account.
|
||||
</p>
|
||||
|
||||
<form
|
||||
hx-put="{{ url_for('payments.update_sumup') }}"
|
||||
hx-target="#payments-panel"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#payments-panel"
|
||||
class="space-y-3"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-stone-600 mb-1">Merchant Code</label>
|
||||
<input
|
||||
type="text"
|
||||
name="merchant_code"
|
||||
value="{{ sumup_merchant_code }}"
|
||||
placeholder="e.g. ME4J6100"
|
||||
class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-stone-600 mb-1">API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
name="api_key"
|
||||
value=""
|
||||
placeholder="{{ '--------' if sumup_configured else 'sup_sk_...' }}"
|
||||
class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
|
||||
>
|
||||
{% if sumup_configured %}
|
||||
<p class="text-xs text-stone-400 mt-0.5">Key is set. Leave blank to keep current key.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-stone-600 mb-1">Checkout Reference Prefix</label>
|
||||
<input
|
||||
type="text"
|
||||
name="checkout_prefix"
|
||||
value="{{ sumup_checkout_prefix }}"
|
||||
placeholder="e.g. ROSE-"
|
||||
class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
|
||||
>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-1.5 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700 focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
Save SumUp Settings
|
||||
</button>
|
||||
|
||||
{% if sumup_configured %}
|
||||
<span class="ml-2 text-xs text-green-600">
|
||||
<i class="fa fa-check-circle"></i> Connected
|
||||
</span>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
2
templates/_types/payments/_nav.html
Normal file
2
templates/_types/payments/_nav.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% from 'macros/admin_nav.html' import placeholder_nav %}
|
||||
{{ placeholder_nav() }}
|
||||
19
templates/_types/payments/_oob_elements.html
Normal file
19
templates/_types/payments/_oob_elements.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends 'oob_elements.html' %}
|
||||
|
||||
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
|
||||
|
||||
{% block oobs %}
|
||||
{% from '_types/root/_n/macros.html' import oob_header with context %}
|
||||
{{oob_header('post-admin-header-child', 'payments-header-child', '_types/payments/header/_header.html')}}
|
||||
|
||||
{% from '_types/post/admin/header/_header.html' import header_row with context %}
|
||||
{{ header_row(oob=True) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block mobile_menu %}
|
||||
{% include '_types/payments/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_types/payments/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
14
templates/_types/payments/header/_header.html
Normal file
14
templates/_types/payments/header/_header.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='payments-row', oob=oob) %}
|
||||
{% call links.link(url_for('payments.home'), hx_select_search) %}
|
||||
<i class="fa fa-credit-card" aria-hidden="true"></i>
|
||||
<div>
|
||||
Payments
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% include '_types/payments/_nav.html' %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
21
templates/_types/payments/index.html
Normal file
21
templates/_types/payments/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends '_types/root/_index.html' %}
|
||||
|
||||
{% block meta %}{% endblock %}
|
||||
|
||||
{% block root_header_child %}
|
||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||
{% call index_row('post-header-child', '_types/post/header/_header.html') %}
|
||||
{% call index_row('payments-header-child', '_types/payments/header/_header.html') %}
|
||||
{% block payments_header_child %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
{% block _main_mobile_menu %}
|
||||
{% include '_types/payments/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/payments/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user