feat: add landing page at / with navigation links
Route GET / to a landing page with the Porchlight logo, tagline, and card-style navigation links to My Account and Administration.
This commit is contained in:
parent
cedf2a65e2
commit
0435b81c5a
4 changed files with 95 additions and 0 deletions
|
|
@ -146,4 +146,8 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
||||||
async def health() -> dict[str, str]:
|
async def health() -> dict[str, str]:
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def landing(request: Request): # type: ignore[no-untyped-def]
|
||||||
|
return templates.TemplateResponse(request, "index.html")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
||||||
|
|
@ -530,6 +530,66 @@ li {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Landing page ---------- */
|
||||||
|
|
||||||
|
.landing {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: var(--sp-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-logo {
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
margin-bottom: var(--sp-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing h1 {
|
||||||
|
margin-bottom: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-blurb {
|
||||||
|
color: var(--fg-muted);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
max-width: 30rem;
|
||||||
|
margin: 0 auto var(--sp-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
max-width: 24rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-link {
|
||||||
|
display: block;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: var(--sp-4) var(--sp-6);
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: left;
|
||||||
|
transition: border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-link:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-link strong {
|
||||||
|
display: block;
|
||||||
|
color: var(--fg);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
margin-bottom: var(--sp-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-link span {
|
||||||
|
color: var(--fg-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Reduced motion ---------- */
|
/* ---------- Reduced motion ---------- */
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|
|
||||||
19
src/porchlight/templates/index.html
Normal file
19
src/porchlight/templates/index.html
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="landing">
|
||||||
|
<img class="landing-logo" src="/static/logo.svg" alt="" width="96" height="96">
|
||||||
|
<h1>Porchlight</h1>
|
||||||
|
<p class="landing-blurb">OpenID Connect Provider with user management. Manage your identity, credentials, and connected applications.</p>
|
||||||
|
<nav class="landing-nav" aria-label="Quick links">
|
||||||
|
<a class="landing-link" href="/manage/profile">
|
||||||
|
<strong>My Account</strong>
|
||||||
|
<span>Manage your profile and credentials</span>
|
||||||
|
</a>
|
||||||
|
<a class="landing-link" href="/admin/users">
|
||||||
|
<strong>Administration</strong>
|
||||||
|
<span>Manage users and settings</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -28,6 +28,18 @@ async def test_app_has_repos_on_state(client: AsyncClient) -> None:
|
||||||
assert isinstance(app.state.magic_link_repo, MagicLinkRepository)
|
assert isinstance(app.state.magic_link_repo, MagicLinkRepository)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_landing_page(client: AsyncClient) -> None:
|
||||||
|
response = await client.get("/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "text/html" in response.headers["content-type"]
|
||||||
|
body = response.text
|
||||||
|
assert "<h1>Porchlight</h1>" in body
|
||||||
|
assert "/manage/profile" in body
|
||||||
|
assert "/admin/users" in body
|
||||||
|
assert "My Account" in body
|
||||||
|
assert "Administration" in body
|
||||||
|
|
||||||
|
|
||||||
async def test_dependency_functions() -> None:
|
async def test_dependency_functions() -> None:
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue