Compare commits
23 Commits
139cf5fa2f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95d954fdb6 | ||
|
|
ac9d97d78d | ||
|
|
e0679f8100 | ||
|
|
0255c937dd | ||
|
|
a4ea5e3bd1 | ||
|
|
c09c433f82 | ||
|
|
249b7d3ff2 | ||
|
|
ab2fa6a393 | ||
|
|
27e6c85e2c | ||
|
|
8c1a0240a3 | ||
|
|
4a707577d3 | ||
|
|
aa63af28fe | ||
|
|
438b2edb0d | ||
|
|
65f14ef0f5 | ||
|
|
dcf716c685 | ||
|
|
99822607a4 | ||
|
|
2f4f8b3e36 | ||
|
|
838f1ae1bc | ||
|
|
75cdb489c4 | ||
|
|
9df5b45696 | ||
|
|
fb0fc5999e | ||
|
|
43dd4d6db4 | ||
|
|
0fb9b1f880 |
@@ -7,7 +7,8 @@ on:
|
|||||||
env:
|
env:
|
||||||
REGISTRY: registry.rose-ash.com:5000
|
REGISTRY: registry.rose-ash.com:5000
|
||||||
IMAGE: events
|
IMAGE: events
|
||||||
REPO_DIR: /root/events
|
REPO_DIR: /root/rose-ash/events
|
||||||
|
COOP_DIR: /root/coop
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
@@ -58,7 +59,7 @@ jobs:
|
|||||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
run: |
|
run: |
|
||||||
ssh "root@$DEPLOY_HOST" "
|
ssh "root@$DEPLOY_HOST" "
|
||||||
cd /root/coop
|
cd ${{ env.COOP_DIR }}
|
||||||
source .env
|
source .env
|
||||||
docker stack deploy -c docker-compose.yml coop
|
docker stack deploy -c docker-compose.yml coop
|
||||||
echo 'Waiting for services to update...'
|
echo 'Waiting for services to update...'
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ RUN pip install -r requirements.txt
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Link app blueprints into the shared library's namespace
|
# Link app blueprints into the shared library's namespace
|
||||||
RUN ln -s /app/bp /app/shared_lib/suma_browser/app/bp
|
RUN rm -rf /app/shared_lib/suma_browser/app/bp && ln -s /app/bp /app/shared_lib/suma_browser/app/bp
|
||||||
|
|
||||||
# ---------- Runtime setup ----------
|
# ---------- Runtime setup ----------
|
||||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
|||||||
88
app.py
88
app.py
@@ -3,12 +3,13 @@ from __future__ import annotations
|
|||||||
import path_setup # noqa: F401 # adds shared_lib to sys.path
|
import path_setup # noqa: F401 # adds shared_lib to sys.path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from quart import g
|
from quart import g, abort
|
||||||
from jinja2 import FileSystemLoader, ChoiceLoader
|
from jinja2 import FileSystemLoader, ChoiceLoader
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from shared.factory import create_base_app
|
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:
|
async def events_context() -> dict:
|
||||||
@@ -40,6 +41,10 @@ async def events_context() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def create_app() -> "Quart":
|
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)
|
app = create_base_app("events", context_fn=events_context)
|
||||||
|
|
||||||
# App-specific templates override shared templates
|
# App-specific templates override shared templates
|
||||||
@@ -49,12 +54,87 @@ def create_app() -> "Quart":
|
|||||||
app.jinja_loader,
|
app.jinja_loader,
|
||||||
])
|
])
|
||||||
|
|
||||||
# Calendars blueprint at root — standalone mode (no post nesting)
|
# Calendars nested under post slug: /<slug>/calendars/...
|
||||||
app.register_blueprint(
|
app.register_blueprint(
|
||||||
register_calendars(),
|
register_calendars(),
|
||||||
url_prefix="/calendars",
|
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):
|
||||||
|
if values and "slug" in values:
|
||||||
|
g.post_slug = values.pop("slug")
|
||||||
|
|
||||||
|
@app.url_defaults
|
||||||
|
def inject_slug(endpoint, values):
|
||||||
|
slug = g.get("post_slug")
|
||||||
|
if slug and "slug" not in values:
|
||||||
|
if app.url_map.is_endpoint_expecting(endpoint, "slug"):
|
||||||
|
values["slug"] = slug
|
||||||
|
|
||||||
|
# --- Load post data for slug ---
|
||||||
|
@app.before_request
|
||||||
|
async def hydrate_post():
|
||||||
|
slug = getattr(g, "post_slug", None)
|
||||||
|
if not slug:
|
||||||
|
return
|
||||||
|
post = (
|
||||||
|
await g.s.execute(
|
||||||
|
select(Post).where(Post.slug == slug)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if not post:
|
||||||
|
abort(404)
|
||||||
|
g.post_data = {
|
||||||
|
"post": {
|
||||||
|
"id": post.id,
|
||||||
|
"title": post.title,
|
||||||
|
"slug": post.slug,
|
||||||
|
"feature_image": post.feature_image,
|
||||||
|
"status": post.status,
|
||||||
|
"visibility": post.visibility,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
async def inject_post():
|
||||||
|
post_data = getattr(g, "post_data", None)
|
||||||
|
if not post_data:
|
||||||
|
return {}
|
||||||
|
post_id = post_data["post"]["id"]
|
||||||
|
calendars = (
|
||||||
|
await g.s.execute(
|
||||||
|
select(Calendar)
|
||||||
|
.where(Calendar.post_id == post_id, Calendar.deleted_at.is_(None))
|
||||||
|
.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
|
# Tickets blueprint — user-facing ticket views and QR codes
|
||||||
from bp.tickets.routes import register as register_tickets
|
from bp.tickets.routes import register as register_tickets
|
||||||
app.register_blueprint(register_tickets())
|
app.register_blueprint(register_tickets())
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
from .calendars.routes import register as register_calendars
|
from .calendars.routes import register as register_calendars
|
||||||
|
from .markets.routes import register as register_markets
|
||||||
|
from .payments.routes import register as register_payments
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def register():
|
|||||||
# ---------- Pages ----------
|
# ---------- Pages ----------
|
||||||
@bp.get("/")
|
@bp.get("/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def admin(slug: str, calendar_slug: str):
|
async def admin(calendar_slug: str, **kwargs):
|
||||||
from suma_browser.app.utils.htmx import is_htmx_request
|
from suma_browser.app.utils.htmx import is_htmx_request
|
||||||
|
|
||||||
# Determine which template to use based on request type
|
# Determine which template to use based on request type
|
||||||
@@ -32,7 +32,7 @@ def register():
|
|||||||
|
|
||||||
@bp.get("/description/")
|
@bp.get("/description/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def calendar_description_edit(slug: str, calendar_slug: str):
|
async def calendar_description_edit(calendar_slug: str, **kwargs):
|
||||||
# g.post and g.calendar should already be set by the parent calendar bp
|
# g.post and g.calendar should already be set by the parent calendar bp
|
||||||
html = await render_template(
|
html = await render_template(
|
||||||
"_types/calendar/admin/_description_edit.html",
|
"_types/calendar/admin/_description_edit.html",
|
||||||
@@ -45,7 +45,7 @@ def register():
|
|||||||
@bp.post("/description/")
|
@bp.post("/description/")
|
||||||
@require_admin
|
@require_admin
|
||||||
@clear_cache(tag="calendars", tag_scope="all")
|
@clear_cache(tag="calendars", tag_scope="all")
|
||||||
async def calendar_description_save(slug: str, calendar_slug: str):
|
async def calendar_description_save(calendar_slug: str, **kwargs):
|
||||||
form = await request.form
|
form = await request.form
|
||||||
description = (form.get("description") or "").strip() or None
|
description = (form.get("description") or "").strip() or None
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ def register():
|
|||||||
|
|
||||||
@bp.get("/description/view/")
|
@bp.get("/description/view/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def calendar_description_view(slug: str, calendar_slug: str):
|
async def calendar_description_view(calendar_slug: str, **kwargs):
|
||||||
# just render the display version without touching the DB (used by Cancel)
|
# just render the display version without touching the DB (used by Cancel)
|
||||||
html = await render_template(
|
html = await render_template(
|
||||||
"_types/calendar/admin/_description.html",
|
"_types/calendar/admin/_description.html",
|
||||||
|
|||||||
@@ -79,10 +79,14 @@ async def create_calendar(sess: AsyncSession, post_id: int, name: str) -> Calend
|
|||||||
slug=slugify(name)
|
slug=slugify(name)
|
||||||
|
|
||||||
# Ensure post exists (avoid silent FK errors in some DBs)
|
# Ensure post exists (avoid silent FK errors in some DBs)
|
||||||
post = (await sess.execute(select(Post.id).where(Post.id == post_id))).scalar_one_or_none()
|
post = (await sess.execute(select(Post).where(Post.id == post_id))).scalar_one_or_none()
|
||||||
if not post:
|
if not post:
|
||||||
raise CalendarError(f"Post {post_id} does not exist.")
|
raise CalendarError(f"Post {post_id} does not exist.")
|
||||||
|
|
||||||
|
# Enforce: calendars can only be created on pages with the calendar feature
|
||||||
|
if not post.is_page:
|
||||||
|
raise CalendarError("Calendars can only be created on pages, not posts.")
|
||||||
|
|
||||||
# Look for existing (including soft-deleted)
|
# Look for existing (including soft-deleted)
|
||||||
q = await sess.execute(
|
q = await sess.execute(
|
||||||
select(Calendar).where(Calendar.post_id == post_id, Calendar.name == name)
|
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
|
||||||
Submodule shared_lib updated: 66edfca7dc...0c9b8d6aa2
@@ -1,8 +1,10 @@
|
|||||||
<!-- Desktop nav -->
|
<!-- Desktop nav -->
|
||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% call links.link(
|
{% call links.link(
|
||||||
|
url_for('calendars.calendar.slots.get', calendar_slug=calendar.slug),
|
||||||
hx_select_search,
|
hx_select_search,
|
||||||
select_colours,
|
select_colours,
|
||||||
|
True,
|
||||||
aclass=styles.nav_button
|
aclass=styles.nav_button
|
||||||
) %}
|
) %}
|
||||||
<i class="fa fa-clock" aria-hidden="true"></i>
|
<i class="fa fa-clock" aria-hidden="true"></i>
|
||||||
@@ -12,4 +14,5 @@
|
|||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% if g.rights.admin %}
|
{% if g.rights.admin %}
|
||||||
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
||||||
|
{{ admin_nav_item(url_for('calendars.calendar.admin.admin', calendar_slug=calendar.slug)) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='calendar-row', oob=oob) %}
|
{% call links.menu_row(id='calendar-row', oob=oob) %}
|
||||||
|
{% call links.link(url_for('calendars.calendar.get', calendar_slug=calendar.slug), hx_select_search) %}
|
||||||
<div class="flex flex-col md:flex-row md:gap-2 items-center min-w-0">
|
<div class="flex flex-col md:flex-row md:gap-2 items-center min-w-0">
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<i class="fa fa-calendar"></i>
|
<i class="fa fa-calendar"></i>
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
{% from '_types/calendar/_description.html' import description %}
|
{% from '_types/calendar/_description.html' import description %}
|
||||||
{{description(calendar)}}
|
{{description(calendar)}}
|
||||||
</div>
|
</div>
|
||||||
|
{% endcall %}
|
||||||
{% call links.desktop_nav() %}
|
{% call links.desktop_nav() %}
|
||||||
{% include '_types/calendar/_nav.html' %}
|
{% include '_types/calendar/_nav.html' %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
@@ -4,9 +4,13 @@
|
|||||||
|
|
||||||
{% block root_header_child %}
|
{% block root_header_child %}
|
||||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||||
{% call index_row('calendar-header-child', '_types/calendar/header/_header.html') %}
|
{% call index_row('post-header-child', '_types/post/header/_header.html') %}
|
||||||
{% block calendar_header_child %}
|
{% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %}
|
||||||
{% endblock %}
|
{% call index_row('calendar-header-child', '_types/calendar/header/_header.html') %}
|
||||||
|
{% block calendar_header_child %}
|
||||||
|
{% endblock %}
|
||||||
|
{% endcall %}
|
||||||
|
{% endcall %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,13 @@
|
|||||||
|
|
||||||
{% block root_header_child %}
|
{% block root_header_child %}
|
||||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||||
{% call index_row('calendars-header-child', '_types/calendars/header/_header.html') %}
|
{% call index_row('post-header-child', '_types/post/header/_header.html') %}
|
||||||
{% block calendars_header_child %}
|
{% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %}
|
||||||
{% endblock %}
|
{% call index_row('calendars-header-child', '_types/calendars/header/_header.html') %}
|
||||||
|
{% block calendars_header_child %}
|
||||||
|
{% endblock %}
|
||||||
|
{% endcall %}
|
||||||
|
{% endcall %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
|
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
|
||||||
{% call(entry_post) scrolling_menu('entry-posts-container', entry_posts) %}
|
{% call(entry_post) scrolling_menu('entry-posts-container', entry_posts) %}
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('blog.post.post_detail', slug=entry_post.slug) }}"
|
href="{{ coop_url('/' + entry_post.slug + '/') }}"
|
||||||
class="flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0">
|
class="flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0">
|
||||||
{% if entry_post.feature_image %}
|
{% if entry_post.feature_image %}
|
||||||
<img src="{{ entry_post.feature_image }}"
|
<img src="{{ entry_post.feature_image }}"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
|
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
|
||||||
{% call(entry_post) scrolling_menu('entry-posts-container', entry_posts) %}
|
{% call(entry_post) scrolling_menu('entry-posts-container', entry_posts) %}
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('blog.post.post_detail', slug=entry_post.slug) }}"
|
href="{{ coop_url('/' + entry_post.slug + '/') }}"
|
||||||
class="{{styles.nav_button}}"
|
class="{{styles.nav_button}}"
|
||||||
>
|
>
|
||||||
{% if entry_post.feature_image %}
|
{% if entry_post.feature_image %}
|
||||||
|
|||||||
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 %}
|
||||||
23
templates/_types/markets/index.html
Normal file
23
templates/_types/markets/index.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% 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('post-admin-header-child', '_types/post/admin/header/_header.html') %}
|
||||||
|
{% call index_row('markets-header-child', '_types/markets/header/_header.html') %}
|
||||||
|
{% block markets_header_child %}
|
||||||
|
{% endblock %}
|
||||||
|
{% endcall %}
|
||||||
|
{% 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 %}
|
||||||
23
templates/_types/payments/index.html
Normal file
23
templates/_types/payments/index.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% 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('post-admin-header-child', '_types/post/admin/header/_header.html') %}
|
||||||
|
{% call index_row('payments-header-child', '_types/payments/header/_header.html') %}
|
||||||
|
{% block payments_header_child %}
|
||||||
|
{% endblock %}
|
||||||
|
{% endcall %}
|
||||||
|
{% endcall %}
|
||||||
|
{% endcall %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block _main_mobile_menu %}
|
||||||
|
{% include '_types/payments/_nav.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include '_types/payments/_main_panel.html' %}
|
||||||
|
{% endblock %}
|
||||||
14
templates/_types/post/_nav.html
Normal file
14
templates/_types/post/_nav.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% import 'macros/links.html' as links %}
|
||||||
|
{% if calendars %}
|
||||||
|
{% for calendar in calendars %}
|
||||||
|
{% call links.link(url_for('calendars.calendar.get', calendar_slug=calendar.slug), hx_select_search, select_colours, True, aclass=styles.nav_button_less_pad) %}
|
||||||
|
<i class="fa fa-calendar" aria-hidden="true"></i>
|
||||||
|
<div>{{ calendar.name }}</div>
|
||||||
|
{% endcall %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% if g.rights.admin %}
|
||||||
|
<a href="{{ coop_url('/' + post.slug + '/admin/') }}" class="{{styles.nav_button}}">
|
||||||
|
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
36
templates/_types/post/admin/_nav.html
Normal file
36
templates/_types/post/admin/_nav.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{% import 'macros/links.html' as links %}
|
||||||
|
<div class="relative nav-group">
|
||||||
|
<a href="{{ events_url('/' + post.slug + '/calendars/') }}" class="{{styles.nav_button}}">
|
||||||
|
calendars
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="relative nav-group">
|
||||||
|
<a href="{{ events_url('/' + post.slug + '/markets/') }}" class="{{styles.nav_button}}">
|
||||||
|
markets
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="relative nav-group">
|
||||||
|
<a href="{{ events_url('/' + post.slug + '/payments/') }}" class="{{styles.nav_button}}">
|
||||||
|
payments
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="relative nav-group">
|
||||||
|
<a href="{{ coop_url('/' + post.slug + '/admin/entries/') }}" class="{{styles.nav_button}}">
|
||||||
|
entries
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="relative nav-group">
|
||||||
|
<a href="{{ coop_url('/' + post.slug + '/admin/data/') }}" class="{{styles.nav_button}}">
|
||||||
|
data
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="relative nav-group">
|
||||||
|
<a href="{{ coop_url('/' + post.slug + '/admin/edit/') }}" class="{{styles.nav_button}}">
|
||||||
|
edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="relative nav-group">
|
||||||
|
<a href="{{ coop_url('/' + post.slug + '/admin/settings/') }}" class="{{styles.nav_button}}">
|
||||||
|
settings
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
12
templates/_types/post/admin/header/_header.html
Normal file
12
templates/_types/post/admin/header/_header.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{% import 'macros/links.html' as links %}
|
||||||
|
{% macro header_row(oob=False) %}
|
||||||
|
{% call links.menu_row(id='post-admin-row', oob=oob) %}
|
||||||
|
<a href="{{ coop_url('/' + post.slug + '/admin/') }}"
|
||||||
|
class="flex items-center gap-2 px-3 py-2 rounded">
|
||||||
|
{{ links.admin() }}
|
||||||
|
</a>
|
||||||
|
{% call links.desktop_nav() %}
|
||||||
|
{% include '_types/post/admin/_nav.html' %}
|
||||||
|
{% endcall %}
|
||||||
|
{% endcall %}
|
||||||
|
{% endmacro %}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='post_entries-row', oob=oob) %}
|
{% call links.menu_row(id='post_entries-row', oob=oob) %}
|
||||||
{% call links.link(url_for('blog.post.admin.entries', slug=post.slug), hx_select_search) %}
|
{% call links.link(coop_url('/' + post.slug + '/admin/entries/'), hx_select_search) %}
|
||||||
<i class="fa fa-clock" aria-hidden="true"></i>
|
<i class="fa fa-clock" aria-hidden="true"></i>
|
||||||
<div>
|
<div>
|
||||||
entries
|
entries
|
||||||
|
|||||||
Reference in New Issue
Block a user