//! 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, IndexTemplate, LibraryTemplate, CartTemplate, OAuthAuthorizeTemplate, ProjectPaywallTemplate, ItemTemplate, LibraryAudioTemplate, LibraryDownloadsTemplate, LibraryLockedTemplate, LibraryTextTemplate, LibraryVideoTemplate, TextReaderTemplate, AudioPlayerTemplate, VideoPlayerTemplate, DiscoverTemplate, DiscoverResultsTemplate, PurchaseTemplate, ReceiptTemplate, BuyPageTemplate, StripeConnectDisclaimerTemplate, // Blog pages BlogPostTemplate, // Documentation pages DocTemplate, DocIndexTemplate, // Platform economics + runway disclosure EconomicsTemplate, // Use cases // Team // Fan+ // Creator invite system // 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 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 // Library tabs LibraryPurchasesTabTemplate, LibraryCollectionsTabTemplate, // Follow button TagFollowToggleTemplate, // Reporting ReportModalTemplate, // Tag suggestions TagSuggestionsTemplate, // Item analytics ItemAnalyticsPartialTemplate, // Item dashboard tabs ItemOverviewTabTemplate, ItemDetailsTabTemplate, ItemPricingTabTemplate, ItemVersionUploadTemplate, ItemEmbedTabTemplate, // Onboarding checklist OnboardingChecklistPartialTemplate, // Tag tree browser TagTreeTemplate, // TOTP 2FA TotpSetupTemplate, TotpStatusTemplate, // Passkeys PasskeyListTemplate, // Git source browser GitRepoTemplate, GitTreeTemplate, GitFileTemplate, GitCommitsTemplate, GitCommitDetailTemplate, GitNotesTemplate, GitAnnotationsTemplate, GitTagsTemplate, GitReplaceTemplate, GitBlameTemplate, 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, );