//! Askama template definitions for all HTML pages and fragments.
//!
//! Split by domain:
//! - `public`: landing, auth, content, blog, discover, health
//! - `dashboard`: creator dashboards, admin, export, account management
//! - `partials`: HTMX fragments, tab content, alerts, form status
mod dashboard;
mod embed;
mod partials;
mod public;
pub use dashboard::*;
pub use embed::*;
pub use partials::*;
pub use public::*;
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse, Response},
};
/// Base context shared by all templates.
/// Note: csrf_token is Option to allow templates to work without CSRF
/// but all authenticated pages should include it.
pub type CsrfTokenOption = Option;
include!(concat!(env!("OUT_DIR"), "/shot_dimensions.rs"));
/// The picture's own size, for a static asset this build read off disk.
///
/// `None` for anything not in the generated table -- a creator's gallery upload,
/// whose dimensions the database never recorded. That is honest: the renderer
/// reserves nothing rather than reserving the wrong thing.
#[must_use]
pub fn shot_size(path: &str) -> Option<(u32, u32)> {
SHOT_DIMENSIONS
.iter()
.find(|(p, _, _)| *p == path)
.map(|&(_, w, h)| (w, h))
}
/// One frame of the composable click-through carousel (`partials/carousel.html`).
///
/// A carousel is just an ordered `&[CarouselFrame]`; the same macro renders it
/// on any surface (app product pages, landing), only the frame list differs.
/// Build a `Vec` on a page template and pass it to the macro.
///
/// Prefer [`CarouselFrame::new`] over a struct literal: it makes the alt text a
/// required, named argument and nudges (in debug builds) toward alt that
/// actually describes the screenshot. A carousel frame is a meaningful image,
/// so alt is not optional and should not be a filename or a bare label like
/// "screenshot". A screen-reader user should get the same information a
/// sighted viewer does.
#[derive(Clone)]
pub struct CarouselFrame {
/// Image URL (typically an optimized screenshot under `/static/images/shots/`).
pub image: String,
/// Alt text describing the screenshot. Required, every frame is an image.
pub alt: String,
/// Optional caption shown under the frame.
pub caption: Option,
/// The image's own pixel dimensions, where this build could learn them.
///
/// Static shots get theirs read off disk by build.rs. A creator's gallery
/// upload gets `None`, because `gallery_images` records a byte count and
/// never recorded a size.
///
/// What it buys: the renderer writes `width`/`height`, the browser holds
/// the frame's place from first paint, and nothing below the carousel moves
/// when the picture lands.
pub intrinsic: Option<(u32, u32)>,
}
impl CarouselFrame {
/// Build a frame, nudging toward helpful alt text.
///
/// In debug builds this asserts the alt text is non-empty and looks like a
/// description rather than a filename or a one-word placeholder. The checks
/// are debug-only so they guide authors during development without ever
/// affecting a release render.
pub fn new(image: impl Into, alt: impl Into) -> Self {
let image = image.into();
let alt = alt.into();
debug_assert!(
!alt.trim().is_empty(),
"carousel frame `{image}` has empty alt text, describe what the \
screenshot shows so screen-reader users get the same information \
sighted viewers do"
);
debug_assert!(
!alt.trim_start().starts_with('/') && !alt.contains(".webp") && !alt.contains(".png"),
"carousel frame alt text looks like a filename (`{alt}`), write a \
human description of what the screenshot shows instead"
);
Self {
// Looked up rather than passed, so every caller that names a static
// shot reserves its space without having to know it did. A path the
// table does not carry -- a creator upload -- stays None, which is
// the honest answer and not a fallback.
intrinsic: shot_size(&image),
image,
alt,
caption: None,
}
}
/// Attach an optional caption shown under the frame.
#[must_use]
pub fn with_caption(mut self, caption: impl Into) -> Self {
self.caption = Some(caption.into());
self
}
}
/// Helper to convert any Askama template into an Axum response.
fn render_template(template: T) -> Response {
match template.render() {
Ok(html) => Html(html).into_response(),
Err(err) => {
tracing::error!(error = ?err, "template rendering error");
(StatusCode::INTERNAL_SERVER_ERROR, "Template error").into_response()
}
}
}
/// Implement `IntoResponse` for one or more Askama template structs.
macro_rules! impl_into_response {
($($T:ty),+ $(,)?) => {
$(
impl IntoResponse for $T {
fn into_response(self) -> Response {
render_template(self)
}
}
)+
};
}
impl_into_response!(
// Public pages
SandboxTemplate,
PolicyTemplate,
IndexTemplate,
LibraryTemplate,
CartTemplate,
LoginTemplate,
TwoFactorTemplate,
OAuthAuthorizeTemplate,
ForgotPasswordTemplate,
ResetPasswordTemplate,
UserTemplate,
ProjectTemplate,
ProjectPaywallTemplate,
ItemTemplate,
LibraryAudioTemplate,
LibraryDownloadsTemplate,
LibraryLockedTemplate,
LibraryTextTemplate,
LibraryVideoTemplate,
TextReaderTemplate,
AudioPlayerTemplate,
VideoPlayerTemplate,
DiscoverTemplate,
DiscoverResultsTemplate,
PurchaseTemplate,
ReceiptTemplate,
BuyPageTemplate,
FeedTemplate,
StripeConnectDisclaimerTemplate,
// Blog pages
ProjectBlogTemplate,
BlogPostTemplate,
// Documentation pages
DocTemplate,
DocIndexTemplate,
// Pricing calculator
PricingTemplate,
FeeCalculatorPartial,
// Platform economics + runway disclosure
EconomicsTemplate,
// Use cases
UseCasesTemplate,
// Team
TeamTemplate,
// Fan+
FanPlusTemplate,
// Creator invite system
CreatorsTemplate,
// Email & account
EmailResultTemplate,
EmailPreferencesTemplate,
ConfirmDeleteTemplate,
AccountDeletedTemplate,
AcknowledgeTemplate,
// Health
HealthTemplate,
// Dashboard pages
DashboardUserTemplate,
DashboardProjectTemplate,
DashboardItemTemplate,
// Admin
AdminWaitlistTemplate,
AdminUsersTemplate,
AdminUploadsTemplate,
AdminScanAuditTemplate,
AdminAppealsTemplate,
AdminReportsTemplate,
AdminSignupsTemplate,
AdminMailCapsTemplate,
AdminMailCapEntriesTemplate,
AdminMetricsTemplate,
AdminCompCodesTemplate,
// Export, import & account management
ExportPortalTemplate,
ImportPortalTemplate,
DeleteAccountTemplate,
BlogEditorTemplate,
// HTMX partials
AlertTemplate,
LibraryStatusTemplate,
ExportDownloadTemplate,
ExportContentReadyTemplate,
TransactionsTableTemplate,
UserProfileTabTemplate,
UserSettingsTabTemplate,
UserAccountTabTemplate,
UserPaymentsTabTemplate,
UserCreatorTabTemplate,
ProjectSettingsTabTemplate,
ProjectCodeTabTemplate,
ProjectSubscriptionsTabTemplate,
ProjectMonetizationTabTemplate,
ItemEditRowTemplate,
// Admin partials
AdminWaitlistEntriesTemplate,
AdminCompCodesEntriesTemplate,
AdminUserEntriesTemplate,
AdminUploadEntriesTemplate,
AdminQueueSummaryTemplate,
AdminAppealEntriesTemplate,
AdminReportEntriesTemplate,
SuspensionBannerTemplate,
// License keys
ItemLicenseKeysTemplate,
// Promo codes
PromoCodesListTemplate,
// Sessions
UserSessionsPartialTemplate,
// SyncKit
UserSyncKitTabTemplate,
ProjectSyncKitTabTemplate,
// Forums (Multithreaded)
// Media library
UserMediaTabTemplate,
// Support
// Collections
CollectionTemplate,
// Library tabs
LibraryPurchasesTabTemplate,
LibraryFeedTabTemplate,
LibraryCollectionsTabTemplate,
// Follow button
FollowButtonTemplate,
TagFollowToggleTemplate,
// Tag suggestions
TagSuggestionsTemplate,
// Item analytics
ItemAnalyticsPartialTemplate,
// Item dashboard tabs
ItemOverviewTabTemplate,
ItemDetailsTabTemplate,
ItemPricingTabTemplate,
ItemFilesTabTemplate,
ItemEmbedTabTemplate,
// Onboarding checklist
OnboardingChecklistPartialTemplate,
// Tag tree browser
TagTreeTemplate,
// TOTP 2FA
TotpSetupTemplate,
TotpStatusTemplate,
// Passkeys
PasskeyListTemplate,
// Git source browser
GitRepoTemplate,
GitTreeTemplate,
GitFileTemplate,
GitCommitsTemplate,
GitCommitDetailTemplate,
GitNotesTemplate,
GitTagsTemplate,
GitReplaceTemplate,
GitBlameTemplate,
GitUserReposTemplate,
GitExploreTemplate,
GitFileLogTemplate,
// Git issues
GitIssueListTemplate,
GitIssueDetailTemplate,
GitRepoSettingsTemplate,
// Join wizard
WizardJoinTemplate,
WizardJoinAccountTemplate,
WizardJoinProfileTemplate,
WizardJoinCompleteTemplate,
// Creation wizards, full pages
WizardProjectTemplate,
WizardItemTemplate,
// Creation wizards, project step partials
WizardProjectBasicsTemplate,
WizardProjectAppearanceTemplate,
WizardProjectMonetizationTemplate,
WizardProjectFirstContentTemplate,
WizardProjectPreviewTemplate,
// Creation wizards, item step partials
WizardItemTypeTemplate,
WizardItemBasicsTemplate,
WizardItemContentTemplate,
WizardItemSectionsTemplate,
WizardItemPricingTemplate,
WizardItemPreviewTemplate,
);