// SPDX-FileCopyrightText: 2026 Ololduck <ololduck@vit.am> and contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
//! The webui's HTML, built from Rust components.
//!
//! A page is a [`Page`]: the document shell, plus the component that makes up
//! its body. A component is any type implementing [`maud::Render`], so the
//! pieces that recur across pages are ordinary Rust types that can be shared,
//! composed and refactored like the rest of the code. Element and attribute
//! names are checked at compile time, and everything interpolated is escaped,
//! so a user name cannot turn into markup.
mod accounts;
pub mod admin;
mod comments;
mod commit;
mod commits;
mod compare;
pub mod components;
mod file;
mod form;
pub mod index;
mod invites;
mod notes;
mod patches;
pub mod profile;
mod refs;
mod remote_forge;
pub mod report;
mod repositories;
mod ssh_keys;
mod tags;
mod tickets;
mod tree;
use actix_web::HttpResponse;
use maud::{DOCTYPE, Render, html};
/// The content type every page is served as, charset included so a user name
/// outside ASCII still decodes.
const HTML: &str = "text/html; charset=utf-8";
/// Consolidates all cross-cutting view parameters into a single context struct.
///
/// This reduces parameter bloat in view functions and makes it clear what
/// information is available to the page. Each field is optional, so a view
/// can render correctly regardless of whether the visitor is logged in, is an
/// admin, or is viewing a repository page.
pub struct ViewCtx {
/// Who is signed in, if anyone. Determines what the navigation offers.
pub user: Option<UserViewParams>,
/// Admin-specific data (badge count), present only for administrators.
pub admin: Option<AdminViewParams>,
/// Repository context, present only when viewing a repository page.
pub repo: Option<RepoViewParams>,
}
impl ViewCtx {
/// Creates a ViewCtx from an optional viewer name.
///
/// This is a temporary helper for the refactor. Later, view functions will
/// take ViewCtx directly instead of individual parameters.
pub fn from_viewer(viewer: Option<&str>) -> Self {
Self {
user: viewer.map(|name| UserViewParams {
id: 0, // TODO: pass user id in later phases
name: name.to_string(),
}),
admin: None, // TODO: wire up admin badge in Phase 1
repo: None, // TODO: wire up repo context in later phases
}
}
}
impl ViewCtx {
/// Creates a context for an anonymous visitor.
pub fn anonymous() -> Self {
Self {
user: None,
admin: None,
repo: None,
}
}
/// Creates a context for a logged-in non-admin user.
pub fn user(id: i64, name: String) -> Self {
Self {
user: Some(UserViewParams { id, name }),
admin: None,
repo: None,
}
}
/// Creates a context for an admin user.
pub fn admin(id: i64, name: String, pending_reports: u64) -> Self {
Self {
user: Some(UserViewParams { id, name }),
admin: Some(AdminViewParams { pending_reports }),
repo: None,
}
}
}
/// Information about the currently logged-in user.
pub struct UserViewParams {
pub id: i64,
pub name: String,
}
/// Admin-specific data for rendering the admin badge in the navigation.
pub struct AdminViewParams {
/// Number of pending reports to show in the badge.
pub pending_reports: u64,
}
/// Context for repository pages, carrying the owner/slug/visibility and
/// which tab is currently active.
pub struct RepoViewParams {
pub owner: String,
pub slug: String,
pub visibility: String,
pub current_tab: RepoTab,
}
/// The tabs available on a repository page.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RepoTab {
Code,
Issues,
PullRequests,
Settings,
}
/// A complete html document: the shell every page is served inside, wrapped
/// around the component that makes up its body.
pub struct Page<B: Render> {
pub title: String,
/// Cross-cutting view context: who's viewing, admin status, repo context.
pub ctx: ViewCtx,
/// Extra HTML to insert into the <head>, e.g. <link rel="alternate">.
pub extra_head: Option<String>,
pub body: B,
}
impl<B: Render> Page<B> {
/// Renders the page and pairs it with the content type it is served as.
pub fn into_response(self) -> HttpResponse {
let extra = self.extra_head.as_deref().unwrap_or("");
let document = html! {
(DOCTYPE)
html lang="en" {
head {
meta charset="utf-8";
meta name="viewport" content="width=device-width";
title { (self.title) }
meta property="og:title" content=(self.title);
link rel="icon" href="/static/frogue-logo.svg" type="image/svg+xml";
link rel="stylesheet" href="/static/style.css" type="text/css";
(maud::PreEscaped(extra))
}
body {
header {
h1 { a href="/" {"🐸 Frogue" span style="color: #ccc" {"(Fed)"} }}
(components::Nav { ctx: &self.ctx })
}
main { (self.body) }
footer {
p {
"Powered by "
a href=(env!("CARGO_PKG_REPOSITORY")) {
em{"Frogue"}
}
" version " code {
(env!("CARGO_PKG_VERSION"))
}
@if let Some(commit_id) = option_env!("GIT_COMMIT_ID") {
code title="git commit short hash" style="color:darkred" {
"-"(commit_id)
}
}
@if let Some(commit_id) = option_env!("JJ_CHANGE_ID") {
code title="jj change short hash" style="color:darkgreen" {
"-"(commit_id)
}
}
}
}
}
}
};
HttpResponse::Ok()
.content_type(HTML)
.body(document.into_string())
}
}
// Re-export public items for backward compatibility
pub use accounts::{LoginForm, RegisterForm, login, register, register_closed};
pub use comments::{CommentAttachment, CommentView};
pub use commit::{commit, commit_header, diff_stats_summary, file_diff, file_diffs};
pub use commits::commits;
pub use compare::{PatchSummary, compare, compare_form};
pub use file::{FileDetail, FileDisplay, file};
pub use index::index;
pub use invites::{InviteSummary, invites};
pub use notes::{
ActionTarget, Note, NoteActions, NoteAttachment, NoteAuthor, NoteAuthorHandle, NotePage,
};
pub use patches::{
PatchDetail, PatchEditForm, PatchRow, PatchesDetail, patch, patch_form, patches,
};
pub use profile::{FollowingEntry, NoteView, RepositorySummary, edit_profile, profile};
pub use refs::refs;
pub use remote_forge::{
NewRemoteTicket, RemoteTicketRow, RemoteTicketsList, new_remote_ticket, remote_tickets,
};
pub use repositories::{
CommitSummary, ReadmeView, RefSummary, RepositoryDetail, RepositoryForm,
RepositorySettingsParams, new_repository, repository, repository_settings,
};
pub use ssh_keys::{SshKeyForm, SshKeySummary, ssh_keys};
pub use tags::tag;
pub use tickets::{
AttachmentView, TicketDetail, TicketForm, TicketRow, TicketsDetail, ticket, ticket_form,
tickets,
};
pub use tree::{TreeDetail, TreeEntry, tree};