//! 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 public; mod dashboard; mod partials; mod embed; pub use public::*; pub use dashboard::*; pub use partials::*; pub use embed::*; 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; /// 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, } 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 { 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, // Platform economics + runway disclosure EconomicsTemplate, // Use cases UseCasesTemplate, // Team TeamTemplate, // Fan+ FanPlusTemplate, // Creator invite system CreatorsTemplate, // Email & account EmailResultTemplate, ConfirmDeleteTemplate, AccountDeletedTemplate, // Health HealthTemplate, // Dashboard pages DashboardUserTemplate, DashboardProjectTemplate, DashboardItemTemplate, // Admin AdminWaitlistTemplate, AdminUsersTemplate, AdminUploadsTemplate, AdminScanAuditTemplate, AdminAppealsTemplate, AdminReportsTemplate, AdminSignupsTemplate, AdminMetricsTemplate, // 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, ProjectSettingsTabTemplate, ProjectCodeTabTemplate, ProjectBlogTabTemplate, ProjectSubscriptionsTabTemplate, ProjectMembersTabTemplate, ProjectMonetizationTabTemplate, ItemEditRowTemplate, // Admin partials AdminWaitlistEntriesTemplate, 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, 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, // Embed widgets EmbedItemButtonTemplate, EmbedItemCardTemplate, EmbedItemPlayerTemplate, EmbedTipButtonTemplate, EmbedProjectCardTemplate, );