//! 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, AdminMetricsTemplate, AdminCompCodesTemplate, // Export, import & account management ExportPortalTemplate, ImportPortalTemplate, DeleteAccountTemplate, BlogEditorTemplate, // HTMX partials AlertTemplate, LibraryStatusTemplate, ExportDownloadTemplate, ExportContentReadyTemplate, TransactionsTableTemplate, UserProfileTabTemplate, UserSettingsTabTemplate, UserAccountTabTemplate, UserSshKeysTabTemplate, UserPaymentsTabTemplate, UserProjectsTabTemplate, UserCreatorTabTemplate, ProjectOverviewTabTemplate, ProjectContentTabTemplate, ProjectAnalyticsTabTemplate, UserAnalyticsTabTemplate, BuyerContactsPartialTemplate, PayoutSummaryPartialTemplate, ProjectSettingsTabTemplate, ProjectCodeTabTemplate, ProjectBlogTabTemplate, ProjectSubscriptionsTabTemplate, ProjectMembersTabTemplate, ProjectMonetizationTabTemplate, ItemEditRowTemplate, // Admin partials AdminWaitlistEntriesTemplate, AdminCompCodesEntriesTemplate, AdminUserEntriesTemplate, AdminUploadEntriesTemplate, AdminQueueSummaryTemplate, AdminAppealEntriesTemplate, AdminReportEntriesTemplate, SuspensionBannerTemplate, // License keys ItemLicenseKeysTemplate, // Promo codes PromoCodesListTemplate, ProjectPromotionsTabTemplate, // Sessions UserSessionsPartialTemplate, // SyncKit UserSyncKitTabTemplate, ProjectSyncKitTabTemplate, // Forums (Multithreaded) UserForumsTabTemplate, // Media library UserMediaTabTemplate, // Support UserSupportTabTemplate, // Collections CollectionTemplate, // Library tabs LibraryPurchasesTabTemplate, LibraryFeedTabTemplate, LibraryCollectionsTabTemplate, LibraryContactsTabTemplate, LibraryCommunitiesTabTemplate, // Follow button FollowButtonTemplate, TagFollowToggleTemplate, // Tag suggestions TagSuggestionsTemplate, // Item analytics ItemAnalyticsPartialTemplate, // Item dashboard tabs ItemOverviewTabTemplate, ItemDetailsTabTemplate, ItemPricingTabTemplate, ItemFilesTabTemplate, ItemSalesTabTemplate, 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, );