Integrate federation app with shared menu/header system
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 44s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 44s
Replace standalone base.html with the shared _types/root layout. Social pages get a second nav row via _types/social/index.html. Root / becomes a blank page with shared chrome. Auth pages use the shared layout without the social nav bar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
17
app.py
17
app.py
@@ -20,10 +20,22 @@ from bp import (
|
|||||||
async def federation_context() -> dict:
|
async def federation_context() -> dict:
|
||||||
"""Federation app context processor."""
|
"""Federation app context processor."""
|
||||||
from shared.infrastructure.context import base_context
|
from shared.infrastructure.context import base_context
|
||||||
|
from shared.services.navigation import get_navigation_tree
|
||||||
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
|
||||||
ctx = await base_context()
|
ctx = await base_context()
|
||||||
|
|
||||||
# If user is logged in, check for ActorProfile
|
ctx["menu_items"] = await get_navigation_tree(g.s)
|
||||||
|
|
||||||
|
# Cart data (consistent with all other apps)
|
||||||
|
ident = current_cart_identity()
|
||||||
|
summary = await services.cart.cart_summary(
|
||||||
|
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
||||||
|
)
|
||||||
|
ctx["cart_count"] = summary.count + summary.calendar_count + summary.ticket_count
|
||||||
|
ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total)
|
||||||
|
|
||||||
|
# Actor profile for logged-in users
|
||||||
if g.get("user"):
|
if g.get("user"):
|
||||||
actor = await services.federation.get_actor_by_user_id(g.s, g.user.id)
|
actor = await services.federation.get_actor_by_user_id(g.s, g.user.id)
|
||||||
ctx["actor"] = actor
|
ctx["actor"] = actor
|
||||||
@@ -60,8 +72,7 @@ def create_app() -> "Quart":
|
|||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def home():
|
async def home():
|
||||||
from quart import render_template
|
from quart import render_template
|
||||||
stats = await services.federation.get_stats(g.s)
|
return await render_template("_types/federation/index.html")
|
||||||
return await render_template("federation/home.html", stats=stats)
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|||||||
3
templates/_types/federation/index.html
Normal file
3
templates/_types/federation/index.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{% extends '_types/root/_index.html' %}
|
||||||
|
{% block meta %}{% endblock %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
52
templates/_types/social/header/_header.html
Normal file
52
templates/_types/social/header/_header.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{% import 'macros/links.html' as links %}
|
||||||
|
{% macro header_row(oob=False) %}
|
||||||
|
{% call links.menu_row(id='social-row', oob=oob) %}
|
||||||
|
<div class="w-full flex flex-row items-center gap-2 flex-wrap">
|
||||||
|
{% if actor %}
|
||||||
|
<nav class="flex gap-3 text-sm items-center flex-wrap">
|
||||||
|
<a href="{{ url_for('social.home_timeline') }}"
|
||||||
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.home_timeline') %}font-bold{% endif %}">
|
||||||
|
Timeline
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('social.public_timeline') }}"
|
||||||
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.public_timeline') %}font-bold{% endif %}">
|
||||||
|
Public
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('social.compose_form') }}"
|
||||||
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.compose_form') %}font-bold{% endif %}">
|
||||||
|
Compose
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('social.following_list') }}"
|
||||||
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.following_list') %}font-bold{% endif %}">
|
||||||
|
Following
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('social.followers_list') }}"
|
||||||
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.followers_list') %}font-bold{% endif %}">
|
||||||
|
Followers
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('social.search') }}"
|
||||||
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.search') %}font-bold{% endif %}">
|
||||||
|
Search
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('social.notifications') }}"
|
||||||
|
class="px-2 py-1 rounded hover:bg-stone-200 relative {% if request.path == url_for('social.notifications') %}font-bold{% endif %}">
|
||||||
|
Notifications
|
||||||
|
<span hx-get="{{ url_for('social.notification_count') }}" hx-trigger="load, every 30s" hx-swap="innerHTML"
|
||||||
|
class="absolute -top-2 -right-3 text-xs bg-red-500 text-white rounded-full px-1 empty:hidden"></span>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('actors.profile', username=actor.preferred_username) }}"
|
||||||
|
class="px-2 py-1 rounded hover:bg-stone-200">
|
||||||
|
@{{ actor.preferred_username }}
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
{% else %}
|
||||||
|
<nav class="flex gap-3 text-sm items-center">
|
||||||
|
<a href="{{ url_for('identity.choose_username_form') }}"
|
||||||
|
class="px-2 py-1 rounded hover:bg-stone-200 font-bold">
|
||||||
|
Choose username
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endcall %}
|
||||||
|
{% endmacro %}
|
||||||
10
templates/_types/social/index.html
Normal file
10
templates/_types/social/index.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{% 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('social-header-child', '_types/social/header/_header.html') %}
|
||||||
|
{% endcall %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
{% block social_content %}{% endblock %}
|
||||||
|
{% endblock %}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/root/_index.html" %}
|
||||||
|
{% block meta %}{% endblock %}
|
||||||
{% block title %}Check your email — Rose Ash{% endblock %}
|
{% block title %}Check your email — Rose Ash{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="py-8 max-w-md mx-auto text-center">
|
<div class="py-8 max-w-md mx-auto text-center">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/root/_index.html" %}
|
||||||
|
{% block meta %}{% endblock %}
|
||||||
{% block title %}Login — Rose Ash{% endblock %}
|
{% block title %}Login — Rose Ash{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="py-8 max-w-md mx-auto">
|
<div class="py-8 max-w-md mx-auto">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/social/index.html" %}
|
||||||
{% block title %}Account — Rose Ash{% endblock %}
|
{% block title %}Account — Rose Ash{% endblock %}
|
||||||
{% block content %}
|
{% block social_content %}
|
||||||
<div class="py-8">
|
<div class="py-8">
|
||||||
<h1 class="text-2xl font-bold mb-4">Account</h1>
|
<h1 class="text-2xl font-bold mb-4">Account</h1>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/social/index.html" %}
|
||||||
|
|
||||||
{% block title %}{{ remote_actor.display_name or remote_actor.preferred_username }} — Rose Ash{% endblock %}
|
{% block title %}{{ remote_actor.display_name or remote_actor.preferred_username }} — Rose Ash{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block social_content %}
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-stone-200 p-6 mb-6">
|
<div class="bg-white rounded-lg shadow-sm border border-stone-200 p-6 mb-6">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
{% if remote_actor.icon_url %}
|
{% if remote_actor.icon_url %}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>{% block title %}Rose Ash{% endblock %}</title>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-stone-50 text-stone-900 min-h-screen">
|
|
||||||
<nav class="bg-stone-800 text-white p-4">
|
|
||||||
<div class="max-w-4xl mx-auto flex items-center justify-between">
|
|
||||||
<a href="/" class="font-bold text-lg">Rose Ash</a>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
{% if g.user %}
|
|
||||||
{% if actor %}
|
|
||||||
<a href="{{ url_for('social.home_timeline') }}" class="hover:underline">Timeline</a>
|
|
||||||
<a href="{{ url_for('social.public_timeline') }}" class="hover:underline">Public</a>
|
|
||||||
<a href="{{ url_for('social.following_list') }}" class="hover:underline">Following</a>
|
|
||||||
<a href="{{ url_for('social.followers_list') }}" class="hover:underline">Followers</a>
|
|
||||||
<a href="{{ url_for('social.search') }}" class="hover:underline">Search</a>
|
|
||||||
<a href="{{ url_for('social.notifications') }}" class="hover:underline relative">
|
|
||||||
Notifications
|
|
||||||
<span hx-get="{{ url_for('social.notification_count') }}" hx-trigger="load, every 30s" hx-swap="innerHTML" class="absolute -top-2 -right-4"></span>
|
|
||||||
</a>
|
|
||||||
<a href="{{ url_for('actors.profile', username=actor.preferred_username) }}" class="hover:underline">
|
|
||||||
@{{ actor.preferred_username }}
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ url_for('identity.choose_username_form') }}" class="hover:underline">Choose username</a>
|
|
||||||
{% endif %}
|
|
||||||
<form method="post" action="{{ url_for('auth.logout') }}" class="inline">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<button type="submit" class="hover:underline">Logout</button>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ url_for('auth.login_form') }}" class="hover:underline">Login</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<main class="max-w-4xl mx-auto p-4">
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/social/index.html" %}
|
||||||
{% block title %}Choose Username — Rose Ash{% endblock %}
|
{% block title %}Choose Username — Rose Ash{% endblock %}
|
||||||
{% block content %}
|
{% block social_content %}
|
||||||
<div class="py-8 max-w-md mx-auto">
|
<div class="py-8 max-w-md mx-auto">
|
||||||
<h1 class="text-2xl font-bold mb-2">Choose your username</h1>
|
<h1 class="text-2xl font-bold mb-2">Choose your username</h1>
|
||||||
<p class="text-stone-600 mb-6">
|
<p class="text-stone-600 mb-6">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/social/index.html" %}
|
||||||
|
|
||||||
{% block title %}Compose — Rose Ash{% endblock %}
|
{% block title %}Compose — Rose Ash{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block social_content %}
|
||||||
<h1 class="text-2xl font-bold mb-6">Compose</h1>
|
<h1 class="text-2xl font-bold mb-6">Compose</h1>
|
||||||
|
|
||||||
<form method="post" action="{{ url_for('social.compose_submit') }}" class="space-y-4">
|
<form method="post" action="{{ url_for('social.compose_submit') }}" class="space-y-4">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/social/index.html" %}
|
||||||
|
|
||||||
{% block title %}Followers — Rose Ash{% endblock %}
|
{% block title %}Followers — Rose Ash{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block social_content %}
|
||||||
<h1 class="text-2xl font-bold mb-6">Followers <span class="text-stone-400 font-normal">({{ total }})</span></h1>
|
<h1 class="text-2xl font-bold mb-6">Followers <span class="text-stone-400 font-normal">({{ total }})</span></h1>
|
||||||
|
|
||||||
<div id="actor-list">
|
<div id="actor-list">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/social/index.html" %}
|
||||||
|
|
||||||
{% block title %}Following — Rose Ash{% endblock %}
|
{% block title %}Following — Rose Ash{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block social_content %}
|
||||||
<h1 class="text-2xl font-bold mb-6">Following <span class="text-stone-400 font-normal">({{ total }})</span></h1>
|
<h1 class="text-2xl font-bold mb-6">Following <span class="text-stone-400 font-normal">({{ total }})</span></h1>
|
||||||
|
|
||||||
<div id="actor-list">
|
<div id="actor-list">
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{% extends "federation/base.html" %}
|
|
||||||
{% block title %}Rose Ash — Federation{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="py-8">
|
|
||||||
<h1 class="text-3xl font-bold mb-4">Rose Ash</h1>
|
|
||||||
<p class="text-stone-600 mb-8">Cooperative platform with ActivityPub federation.</p>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
|
||||||
<div class="text-3xl font-bold">{{ stats.actors }}</div>
|
|
||||||
<div class="text-stone-500">Actors</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
|
||||||
<div class="text-3xl font-bold">{{ stats.activities }}</div>
|
|
||||||
<div class="text-stone-500">Activities</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
|
||||||
<div class="text-3xl font-bold">{{ stats.followers }}</div>
|
|
||||||
<div class="text-stone-500">Followers</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/social/index.html" %}
|
||||||
|
|
||||||
{% block title %}Notifications — Rose Ash{% endblock %}
|
{% block title %}Notifications — Rose Ash{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block social_content %}
|
||||||
<h1 class="text-2xl font-bold mb-6">Notifications</h1>
|
<h1 class="text-2xl font-bold mb-6">Notifications</h1>
|
||||||
|
|
||||||
{% if not notifications %}
|
{% if not notifications %}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/social/index.html" %}
|
||||||
{% block title %}@{{ actor.preferred_username }} — Rose Ash{% endblock %}
|
{% block title %}@{{ actor.preferred_username }} — Rose Ash{% endblock %}
|
||||||
{% block content %}
|
{% block social_content %}
|
||||||
<div class="py-8">
|
<div class="py-8">
|
||||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||||
<h1 class="text-2xl font-bold">{{ actor.display_name or actor.preferred_username }}</h1>
|
<h1 class="text-2xl font-bold">{{ actor.display_name or actor.preferred_username }}</h1>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/social/index.html" %}
|
||||||
|
|
||||||
{% block title %}Search — Rose Ash{% endblock %}
|
{% block title %}Search — Rose Ash{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block social_content %}
|
||||||
<h1 class="text-2xl font-bold mb-6">Search</h1>
|
<h1 class="text-2xl font-bold mb-6">Search</h1>
|
||||||
|
|
||||||
<form method="get" action="{{ url_for('social.search') }}" class="mb-6">
|
<form method="get" action="{{ url_for('social.search') }}" class="mb-6">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "federation/base.html" %}
|
{% extends "_types/social/index.html" %}
|
||||||
|
|
||||||
{% block title %}{{ "Home" if timeline_type == "home" else "Public" }} Timeline — Rose Ash{% endblock %}
|
{% block title %}{{ "Home" if timeline_type == "home" else "Public" }} Timeline — Rose Ash{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block social_content %}
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="text-2xl font-bold">{{ "Home" if timeline_type == "home" else "Public" }} Timeline</h1>
|
<h1 class="text-2xl font-bold">{{ "Home" if timeline_type == "home" else "Public" }} Timeline</h1>
|
||||||
{% if actor %}
|
{% if actor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user