Added Home page

This commit is contained in:
Jim Lancaster
2026-08-04 19:20:33 -05:00
parent d9f5fbb1a4
commit 4eeb552273
7 changed files with 205 additions and 8 deletions
+2 -2
View File
@@ -86,11 +86,11 @@ def create_app(settings: Settings | None = None) -> FastAPI:
@app.get("/", include_in_schema=False)
async def root_redirect() -> RedirectResponse:
return RedirectResponse(url="/ui", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
return RedirectResponse(url="/ui/homepage", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
@app.get("/ui", include_in_schema=False)
async def ui_redirect() -> RedirectResponse:
return RedirectResponse(url="/ui/documents", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
return RedirectResponse(url="/ui/homepage", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
@app.get("/healthz")
def health() -> dict[str, str]:
+2
View File
@@ -3,6 +3,7 @@
from fastapi import FastAPI
from nicegui import ui
from transcription.ui.pages.home_page import register_page as register_home_page
from transcription.ui.pages.documents_page import register_page as register_documents_page
from transcription.ui.pages.jobs_page import register_page as register_jobs_page
from transcription.ui.pages.people_page import register_page as register_people_page
@@ -25,6 +26,7 @@ def _register_global_styles(app: FastAPI) -> None:
def register_pages(app: FastAPI) -> None:
"""Register all NiceGUI pages and mount them onto the FastAPI app."""
_register_global_styles(app)
register_home_page()
register_upload_page()
register_documents_page()
register_people_page()
+4 -2
View File
@@ -43,7 +43,7 @@ def _render_nav_button(*, label: str, path: str, icon: str, current_path: str) -
def _normalize_path(current_path: str | None) -> str:
normalized = (current_path or "").strip()
if not normalized:
return "/jobs"
return "/homepage"
return normalized.rstrip("/") or "/"
@@ -53,7 +53,9 @@ def render_app_shell(*, current_path: str | None = None) -> None:
normalized_path = _normalize_path(current_path)
with ui.header().classes("app-shell"), ui.element("div").classes("app-shell__inner"):
with ui.row().classes("app-shell__brand no-wrap"):
with ui.element("a").props('href="/ui/homepage"').style(
"display:flex; align-items:center; gap:0.75rem; text-decoration:none; color:inherit;"
).classes("app-shell__brand no-wrap"):
ui.html(VIBESCRIBE_LOGO_SVG).classes("app-shell__brand-mark")
ui.label("VibeScribe").classes("app-shell__brand-name")
+62
View File
@@ -0,0 +1,62 @@
"""File-backed storage helpers for the homepage content."""
from __future__ import annotations
from pathlib import Path
HOME_PAGE_DIR = Path(__file__).resolve().parents[3] / "data" / "homepage"
HOME_PAGE_MARKDOWN_PATH = HOME_PAGE_DIR / "homepage.md"
SUPPORTED_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tif", ".tiff"}
def ensure_homepage_storage() -> None:
"""Create the homepage storage directory when needed."""
HOME_PAGE_DIR.mkdir(parents=True, exist_ok=True)
def read_homepage_markdown() -> str:
"""Read the saved homepage markdown text."""
ensure_homepage_storage()
if not HOME_PAGE_MARKDOWN_PATH.exists():
return ""
return HOME_PAGE_MARKDOWN_PATH.read_text(encoding="utf-8")
def save_homepage_markdown(markdown_text: str) -> None:
"""Persist the homepage markdown text."""
ensure_homepage_storage()
HOME_PAGE_MARKDOWN_PATH.write_text(markdown_text, encoding="utf-8")
def store_homepage_image(*, filename: str, file_bytes: bytes) -> Path:
"""Persist an uploaded homepage image in the shared homepage folder."""
ensure_homepage_storage()
safe_name = Path(filename).name
if not safe_name:
msg = "Homepage image filename is required"
raise ValueError(msg)
stored_path = HOME_PAGE_DIR / safe_name
stored_path.write_bytes(file_bytes)
return stored_path
def list_homepage_images() -> list[Path]:
"""List stored homepage images in the order they were last updated."""
ensure_homepage_storage()
image_paths = [
path
for path in HOME_PAGE_DIR.iterdir()
if path.is_file() and path.suffix.lower() in SUPPORTED_IMAGE_SUFFIXES
]
return sorted(image_paths, key=lambda path: (path.stat().st_mtime, path.name))
def latest_homepage_image() -> Path | None:
"""Return the most recently updated homepage image, if one exists."""
image_paths = list_homepage_images()
if not image_paths:
return None
return image_paths[-1]
+110
View File
@@ -0,0 +1,110 @@
"""Homepage registration and handlers."""
from __future__ import annotations
from nicegui import ui
from transcription.ui.components.app_shell import render_navigation_header
from transcription.ui.components.cards import archival_card
from transcription.ui.components.primitives import render_empty_state
from transcription.ui.components.primitives import section_header_row
from transcription.ui.components.viewers import dark_room_viewer
from transcription.ui.homepage_store import latest_homepage_image
from transcription.ui.homepage_store import read_homepage_markdown
from transcription.ui.homepage_store import save_homepage_markdown
from transcription.ui.homepage_store import store_homepage_image
from transcription.ui.theme import apply_archival_theme
from transcription.ui.theme import page_header
def _render_homepage_view(*, markdown_text: str, image_path) -> None:
with ui.grid().classes("w-full grid-cols-12 gap-4"):
with ui.column().classes("col-span-12 lg:col-span-4"):
dark_room_viewer(str(image_path) if image_path else None, count_label="Homepage Image")
with ui.column().classes("col-span-12 lg:col-span-5 gap-4"), archival_card(title="Home Text"):
if markdown_text:
ui.markdown(markdown_text)
else:
render_empty_state("No homepage text saved yet.")
with ui.column().classes("col-span-12 lg:col-span-3"):
ui.element("div")
def _render_homepage_editor(*, render_image_panel, markdown_input, on_upload) -> None:
with ui.grid().classes("w-full grid-cols-12 gap-4"):
with ui.column().classes("col-span-12 lg:col-span-4 gap-4"):
with archival_card(title="Homepage Image"):
ui.upload(on_upload=on_upload, auto_upload=True, label="Upload image").props(
'accept=".jpg,.jpeg,.png,.gif,.webp,.bmp,.tif,.tiff"'
).classes("w-full")
render_image_panel()
with ui.column().classes("col-span-12 lg:col-span-5 gap-4"), archival_card(title="Home Text"):
markdown_input[0] = ui.textarea(
label="Homepage markdown",
value=read_homepage_markdown(),
).props("outlined autogrow").classes("w-full")
with ui.column().classes("col-span-12 lg:col-span-3"):
ui.element("div")
def register_page() -> None:
"""Register the homepage routes."""
@ui.page("/homepage", title="VibeScribe Home")
def homepage_page() -> None:
apply_archival_theme()
render_navigation_header(current_path="/homepage")
with ui.column().classes("w-full max-w-[1800px] mx-auto p-4 gap-4"):
with section_header_row():
page_header("Home")
ui.button(
"Edit Home Page",
on_click=lambda: ui.navigate.to("/homepage/edit"),
icon="edit",
).classes("ui-btn-primary text-xs")
_render_homepage_view(
markdown_text=read_homepage_markdown().strip(),
image_path=latest_homepage_image(),
)
@ui.page("/homepage/edit", title="Edit Homepage")
def homepage_edit_page() -> None:
apply_archival_theme()
render_navigation_header(current_path="/homepage")
preview_image = [latest_homepage_image()]
markdown_input = [None]
@ui.refreshable
def render_image_panel() -> None:
dark_room_viewer(str(preview_image[0]) if preview_image[0] else None, count_label="Homepage Image")
async def on_upload(event) -> None:
payload = await event.file.read()
preview_image[0] = store_homepage_image(filename=event.file.name, file_bytes=payload)
ui.notify(f"Uploaded {event.file.name}", type="positive")
render_image_panel.refresh()
async def save_homepage() -> None:
save_homepage_markdown((markdown_input[0].value if markdown_input[0] is not None else "") or "")
ui.notify("Homepage saved", type="positive")
ui.navigate.to("/homepage")
with ui.column().classes("w-full max-w-[1800px] mx-auto p-4 gap-4"):
with section_header_row():
page_header("Edit Home Page")
with ui.row().classes("items-center gap-2"):
ui.button("Save", on_click=save_homepage, icon="save").classes("ui-btn-primary text-xs")
ui.button("Cancel", on_click=lambda: ui.navigate.to("/homepage"), icon="close").props("flat")
_render_homepage_editor(
render_image_panel=render_image_panel,
markdown_input=markdown_input,
on_upload=on_upload,
)
+2
View File
@@ -11,12 +11,14 @@ class TestPageRegistration:
"""Mounted UI routes respond successfully when the full app is created."""
_, client = app_client
homepage_response = client.get("/ui/homepage")
upload_response = client.get("/ui/upload", follow_redirects=False)
documents_response = client.get("/ui/documents")
people_response = client.get("/ui/people")
sources_response = client.get("/ui/sources")
jobs_response = client.get("/ui/jobs")
assert homepage_response.status_code == 200
assert upload_response.status_code == 307
assert documents_response.status_code == 200
assert people_response.status_code == 200
+23 -4
View File
@@ -13,15 +13,34 @@ class TestPageRendering:
response = client.get("/", follow_redirects=False)
assert response.status_code == 307
assert response.headers["location"] == "/ui"
assert response.headers["location"] == "/ui/homepage"
def test_ui_redirects_to_documents(self, app_client):
"""GET /ui redirects to the documents page."""
def test_ui_redirects_to_homepage(self, app_client):
"""GET /ui redirects to the homepage."""
_, client = app_client
response = client.get("/ui", follow_redirects=False)
assert response.status_code == 307
assert response.headers["location"] == "/ui/documents"
assert response.headers["location"] == "/ui/homepage"
def test_homepage_page_renders(self, app_client):
"""GET /ui/homepage renders the homepage page."""
_, client = app_client
response = client.get("/ui/homepage")
assert response.status_code == 200
assert "Home" in response.text
assert "Edit Home Page" in response.text
assert '/ui/homepage' in response.text
def test_homepage_edit_page_renders(self, app_client):
"""GET /ui/homepage/edit renders the edit page."""
_, client = app_client
response = client.get("/ui/homepage/edit")
assert response.status_code == 200
assert "Edit Home Page" in response.text
assert "Homepage markdown" in response.text
def test_upload_page_renders_expected_controls(self, app_client):
"""GET /ui/upload redirects to the job-create flow."""